Gather Intel - Amass Intel | Online Free DevTools by Hexmos

Gather open-source intel with Amass Intel. Discover root domains and ASNs related to organizations and IP addresses. Free online tool, no registration required.

amass intel

+
+

Collect open source intel on an organisation like root domains and ASNs. +More information: https://github.com/owasp-amass/amass/blob/master/doc/user_guide.md#the-intel-subcommand.

+
+
    +
  • Find root domains in an IP [addr]ess range:
  • +
+

amass intel -addr {{192.168.0.1-254}}

+
    +
  • Use active recon methods:
  • +
+

amass intel -active -addr {{192.168.0.1-254}}

+
    +
  • Find root domains related to a [d]omain:
  • +
+

amass intel -whois -d {{domain_name}}

+
    +
  • Find ASNs belonging to an [org]anisation:
  • +
+

amass intel -org {{organisation_name}}

+
    +
  • Find root domains belonging to a given Autonomous System Number:
  • +
+

amass intel -asn {{asn}}

+
    +
  • Save results to a text file:
  • +
+

amass intel -o {{output_file}} -whois -d {{domain_name}}

+
    +
  • List all available data sources:
  • +
+

amass intel -list

See Also

diff --git a/frontend/integrations/copy-worker.mjs b/frontend/integrations/copy-worker.mjs new file mode 100644 index 0000000000..36f3010298 --- /dev/null +++ b/frontend/integrations/copy-worker.mjs @@ -0,0 +1,94 @@ +import fs from 'fs'; +import path from 'path'; + +/** + * Astro integration to compile and copy worker.ts files to dist directory during build + * This ensures the worker files are available at runtime in production as JavaScript + */ +export function copyWorkerFile() { + return { + name: 'copy-worker-file', + hooks: { + 'astro:build:done': async ({ dir }) => { + const projectRoot = process.cwd(); + // Always use project root's dist directory, not the dir parameter which may point to client + const distDir = path.join(projectRoot, 'dist'); + + const workers = [ + { + source: path.join(projectRoot, 'db', 'svg_icons', 'svg-worker.ts'), + dist: path.join(distDir, 'server', 'chunks', 'db', 'svg_icons', 'svg-worker.js'), + name: 'SVG', + }, + { + source: path.join(projectRoot, 'db', 'png_icons', 'png-worker.ts'), + dist: path.join(distDir, 'server', 'chunks', 'db', 'png_icons', 'png-worker.js'), + name: 'PNG', + }, + { + source: path.join(projectRoot, 'db', 'emojis', 'emoji-worker.ts'), + dist: path.join(distDir, 'server', 'chunks', 'db', 'emojis', 'emoji-worker.js'), + name: 'EMOJI', + }, + { + source: path.join(projectRoot, 'db', 'cheatsheets', 'cheatsheets-worker.ts'), + dist: path.join(distDir, 'server', 'chunks', 'db', 'cheatsheets', 'cheatsheets-worker.js'), + name: 'CHEATSHEETS', + }, + { + source: path.join(projectRoot, 'db', 'man_pages', 'man-pages-worker.ts'), + dist: path.join(distDir, 'server', 'chunks', 'db', 'man_pages', 'man-pages-worker.js'), + name: 'MAN_PAGES', + }, + { + source: path.join(projectRoot, 'db', 'mcp', 'mcp-worker.ts'), + dist: path.join(distDir, 'server', 'chunks', 'db', 'mcp', 'mcp-worker.js'), + name: 'MCP', + }, + { + source: path.join(projectRoot, 'db', 'tldrs', 'tldr-worker.ts'), + dist: path.join(distDir, 'server', 'chunks', 'db', 'tldrs', 'tldr-worker.js'), + name: 'TLDR', + }, + ]; + + // Try to use esbuild (available through Vite) + const esbuild = await import('esbuild').catch(() => null); + if (!esbuild) { + throw new Error('esbuild not available'); + } + + for (const worker of workers) { + // Create directory structure if it doesn't exist + const distWorkerDir = path.dirname(worker.dist); + if (!fs.existsSync(distWorkerDir)) { + fs.mkdirSync(distWorkerDir, { recursive: true }); + } + + // Check if source file exists + if (!fs.existsSync(worker.source)) { + console.warn(`⚠️ ${worker.name} worker file not found at ${worker.source}`); + continue; + } + + try { + await esbuild.default.build({ + entryPoints: [worker.source], + outfile: worker.dist, + format: 'esm', + target: 'node18', + bundle: false, + platform: 'node', + sourcemap: false, + }); + console.log(`✅ Compiled ${worker.name} worker.js using esbuild to ${worker.dist}`); + } catch (error) { + console.error(`❌ Failed to compile ${worker.name} worker.ts with esbuild: ${error.message}`); + throw new Error(`${worker.name} worker compilation failed. Please ensure esbuild is available or compile manually.`); + } + } + }, + }, + }; +} + diff --git a/frontend/PAGESPEED.md b/frontend/md/PAGESPEED.md similarity index 100% rename from frontend/PAGESPEED.md rename to frontend/md/PAGESPEED.md diff --git a/frontend/md/bun_to_node.md b/frontend/md/bun_to_node.md new file mode 100644 index 0000000000..3c76eb68d0 --- /dev/null +++ b/frontend/md/bun_to_node.md @@ -0,0 +1,128 @@ +# Migrate from Bun to Node.js + +## Overview + +Replace Bun runtime with Node.js by: + +1. Replacing `bun:sqlite` with `better-sqlite3` in database code +2. Updating all package.json scripts to use `node`/`npm` instead of `bun` +3. Cleaning build artifacts and reinstalling dependencies + +## Files to Modify + +### Database Code Changes + +**`db/svg_icons/svg-worker.ts`** + +- Replace `import { Database } from 'bun:sqlite'` with `import Database from 'better-sqlite3'` +- Update database initialization: `new Database(dbPath)` (note: `{ readonly: true }` option is NOT supported in better-sqlite3) +- Use `db.exec('PRAGMA ...')` instead of `db.pragma()` for setting PRAGMA values +- Update comment on line 2 to reflect better-sqlite3 +- Add error handling around database initialization + +**`db/banner/banner-utils.ts`** + +- Replace `import { Database } from 'bun:sqlite'` with `import Database from 'better-sqlite3'` +- Update database initialization: `new Database(dbPath)` (note: `{ readonly: true }` option is NOT supported) +- Change PRAGMA calls from `db.run('PRAGMA ...')` to `db.exec('PRAGMA ...')` +- The `db.run()` method works the same way for regular SQL statements + +**`db/svg_icons/svg-worker-pool.ts`** + +- Update comment on line 2 to reflect better-sqlite3 instead of bun:sqlite + +### Package Configuration + +**`package.json`** + +- Add `better-sqlite3` to dependencies +- Add `@types/better-sqlite3` to devDependencies +- Update scripts: +- `dev`: Change from `bun --max-old-space-size=16384` to `node --max-old-space-size=16384` +- `dev:light`: Change from `bun --max-old-space-size=4096` to `node --max-old-space-size=4096` +- `build`: Change from `bun run` to `node` +- `build:mcp`, `build:tldr`, `build:icons`, `build:emojis`, `build:man-pages`, `build:index`: Change from `bun` to `node` +- `serve-ssr`: Change from `bun run` to `node` +- `banner:generate`, `pagespeed*`: Change from `bun run` to `node` +- Remove `@types/bun` from devDependencies (if present) + +### No Changes Needed + +- `integrations/copy-worker.mjs` - No bun-specific code +- `integrations/critical-css-inlining.mjs` - No bun-specific code +- `integrations/wrap-astro.mjs` - No bun-specific code +- `src/middleware.ts` - No bun-specific code +- `astro.config.mjs` - No bun-specific code + +## Execution Steps + +1. Remove build artifacts: `node_modules`, `dist`, `.astro` +2. Update all files listed above +3. Run `npm install` to install dependencies including better-sqlite3 + - Note: If you encounter CUDA-related errors with `onnxruntime-node`, use: `ONNXRUNTIME_NODE_INSTALL_CUDA=skip npm install` +4. **Compile worker files for development** (required for Node.js): + ```bash + npx esbuild db/svg_icons/svg-worker.ts --outfile=db/svg_icons/svg-worker.js --format=esm --target=node18 --bundle=false --platform=node + ``` + + - This creates a `.js` file that the worker pool can load in development mode + - The worker pool looks for `.js` files first, then falls back to `.ts` files +5. Start server: `npm run dev` +6. Wait 30 seconds, then test: `curl localhost:4321/freedevtools/svg_icons/` +7. If test fails, check console logs and fix issues iteratively + +## Important Differences from bun:sqlite + +### Database Initialization + +- **bun:sqlite**: `new Database(dbPath, { readonly: true })` - supports readonly option +- **better-sqlite3**: `new Database(dbPath)` - readonly option NOT supported, use `PRAGMA query_only = ON` instead + +### PRAGMA Usage + +- **bun:sqlite**: Can use `db.run('PRAGMA ...')` or `db.pragma('key', 'value')` +- **better-sqlite3**: Must use `db.exec('PRAGMA key = value')` - the `pragma()` method is for reading values, not setting them + +### API Compatibility + +- `stmt.get()`, `stmt.all()`, and `db.run()` methods are compatible +- `db.prepare()` works the same way +- `better-sqlite3` API is synchronous and compatible with the current code structure + +## Worker File Compilation + +### Current State + +- Worker files (`.ts`) must be compiled to JavaScript (`.js`) for Node.js to execute them +- The `copy-worker.mjs` integration only compiles workers during build (`astro:build:done` hook) +- In development, you must manually compile worker files before starting the dev server + +### Manual Compilation + +```bash +npx esbuild db/svg_icons/svg-worker.ts --outfile=db/svg_icons/svg-worker.js --format=esm --target=node18 --bundle=false --platform=node +``` + +### Future Automation (Recommended) + +The `copy-worker.mjs` integration could be enhanced to also compile workers in development mode by adding an `astro:server:setup` or `astro:config:setup` hook. This would: + +- Automatically compile worker files when the dev server starts +- Watch for changes and recompile on file modifications +- Eliminate the need for manual compilation + +Example enhancement to `copy-worker.mjs`: + +```javascript +hooks: { + 'astro:server:setup': async ({ server }) => { + // Compile workers for development + await compileWorkers(); + }, + 'astro:build:done': async ({ dir }) => { + // Existing build-time compilation + } +} +``` + +This would ensure worker files are always available in both development and production environments. diff --git a/frontend/design.md b/frontend/md/design.md similarity index 100% rename from frontend/design.md rename to frontend/md/design.md diff --git a/frontend/md/eventListnerIssue.md b/frontend/md/eventListnerIssue.md new file mode 100644 index 0000000000..51f54cc446 --- /dev/null +++ b/frontend/md/eventListnerIssue.md @@ -0,0 +1,276 @@ +``` +[stdout] [Auth Middleware] JWT found, allowing request to proceed +[stdout] [2025-12-06T10:01:18.634Z] [EMOJI_DB] Worker 1 completed getCategoriesWithPreviewEmojis in 25ms +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 handling getTotalEmojis +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 completed getTotalEmojis in 0ms +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 handling getEmojiCategories +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 completed getEmojiCategories in 0ms +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 handling getEmojiCategories +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 completed getEmojiCategories in 0ms +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 handling getEmojiCategories +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 completed getEmojiCategories in 0ms +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 handling getEmojiCategories +[stdout] [2025-12-06T10:01:18.635Z] [EMOJI_DB] Worker 1 completed getEmojiCategories in 0ms +[stderr] MaxListenersExceededWarning: Possible EventTarget memory leak detected. 101 message listeners added to [Worker]. MaxListeners is 100. Use events.setMaxListeners() to increase limit +[stderr] emitter: Worker { +[stderr] \_events: [Object ...], +[stderr] \_eventsCount: 3, +[stderr] \_maxListeners: 100, +[stderr] [Symbol(kCapture)]: false, +[stderr] threadId: [Getter], +[stderr] ref: [Function: ref], +[stderr] unref: [Function: unref], +[stderr] stdin: [Getter], +[stderr] stdout: [Getter], +[stderr] stderr: [Getter], +[stderr] performance: [Getter], +[stderr] terminate: [Function: terminate], +[stderr] postMessage: [Function: postMessage], +[stderr] getHeapSnapshot: [Function: getHeapSnapshot], +[stderr] [Symbol(Symbol.asyncDispose)]: [AsyncFunction], +[stderr] setMaxListeners: [Function: setMaxListeners], +[stderr] getMaxListeners: [Function: getMaxListeners], +[stderr] emit: [Function: emit], +[stderr] addListener: [Function: addListener], +[stderr] on: [Function: addListener], +[stderr] prependListener: [Function: prependListener], +[stderr] once: [Function: once], +[stderr] prependOnceListener: [Function: prependOnceListener], +[stderr] removeListener: [Function: removeListener], +[stderr] off: [Function: removeListener], +[stderr] removeAllListeners: [Function: removeAllListeners], +[stderr] listeners: [Function: listeners], +[stderr] rawListeners: [Function: rawListeners], +[stderr] listenerCount: [Function: listenerCount], +[stderr] eventNames: [Function: eventNames], +[stderr] }, +[stderr] type: "message", +[stderr] count: 101, +[stderr] +[stderr] at overflowWarning (node:events:185:14) +[stderr] at addListener (node:events:158:22) +[stderr] at (/home/ubuntu/FreeDevTools/frontend/dist/server/chunks/emojis-utils_BonZaL4u.mjs:756:12) +[stderr] at new Promise (1:11) +[stderr] at executeQuery (/home/ubuntu/FreeDevTools/frontend/dist/server/chunks/emojis-utils_BonZaL4u.mjs:736:10) +[stderr] + +[stdout] [Auth Middleware] ENABLE_SIGNIN env value: "true", enabled: true, Path: /freedevtools/emojis/smileys-emotion/ +[stdout] [Auth Middleware] Is static asset: false +[stdout] [Auth Middleware] Has ?data= param: false +[stdout] [Auth Middleware] JWT from Authorization header: missing +[stdout] [Auth Middleware] JWT from cookie: present +[stdout] [Auth Middleware] JWT found, allowing request to proceed +[stdout] [EMOJI_DB][2025-12-06T10:01:18.820Z] Dispatching getEmojiCategories +[stdout] [EMOJI_DB][2025-12-06T10:01:18.834Z] getDiscordCategoriesWithPreviewEmojis completed in 650ms +[stdout] [EMOJI_DB][2025-12-06T10:01:18.834Z] Dispatching getEmojiCategories +[stdout] [EMOJI_DB][2025-12-06T10:01:18.834Z] getEmojiCategories completed in 628ms +[stdout] [2025-12-06T10:01:18.835Z] [EMOJI_DB] Worker 1 handling getEmojiCategories +[stdout] [2025-12-06T10:01:18.835Z] [EMOJI_DB] Worker 1 completed getEmojiCategories in 0ms +[stdout] [EMOJI_DB][2025-12-06T10:01:18.836Z] Dispatching fetchImageFromDB +[stdout] [EMOJI_DB][2025-12-06T10:01:18.836Z] getEmojiBySlug completed in 630ms +[stdout] [EMOJI_DB][2025-12-06T10:01:18.836Z] Dispatching getEmojisByCategoryWithDiscordImagesPaginated +[stdout] [EMOJI_DB][2025-12-06T10:01:18.837Z] getEmojiBySlug completed in 631ms +[stdout] [EMOJI_DB][2025-12-06T10:01:18.837Z] Dispatching getEmojiImages +[stdout] [EMOJI_DB][2025-12-06T10:01:18.837Z] getEmojiBySlug completed in 631ms +[stdout] [EMOJI_DB]2025-12-06T10:01: +``` + +once this max listner thing is hit +DB query time is exploding + +shrijith - 3:36 PM +what is the WORKER_COUNT +how much is i t in this case +ok this 100 is per listener + +lovestaco — 3:36 PM +2 worker per category +3 categories are running +2 instances of server running + +shrijith — 3:36 PM +ok +2 instances - in separate cores - hopefully + +lovestaco — 3:37 PM +yes have added affinity +cpu 1 - 4321 +cpu 2 - 4322 + +chatgpt response +This warning is coming from **Node.js EventEmitter** system. It’s not a crash, it’s telling you something is probably wrong in your code design. + +--- + +## **What the warning means** + +``` +MaxListenersExceededWarning: Possible EventTarget memory leak detected. +101 message listeners added to [Worker]. MaxListeners is 100. +``` + +### Translation in simple terms: + +You have a **Worker thread** (Node.js Worker) where you're adding **"message" event listeners** again and again (101 times). +Normally, Node warns you when more than 100 listeners are attached to the same event, because that is usually a **bug that causes memory leaks**. + +Example of what is happening conceptually: + +```js +worker.on('message', () => { + // handle message +}); +``` + +If your code runs this inside a loop, or inside a request handler, or repeatedly, you’ll end up with 101 listeners → **memory leak detected**. + +--- + +## **Where it’s coming from** + +The trace says: + +``` +/dist/server/chunks/emojis-utils_BonZaL4u.mjs:756:12 +at executeQuery (/dist/server/chunks/emojis-utils_BonZaL4u.mjs:736:10) +``` + +So likely inside `executeQuery()`, every time it runs, it creates a new worker and attaches a `.on("message")` listener without removing the old ones. + +--- + +## **Why Node has max listeners** + +Because if you do something like: + +```js +setInterval(() => { + worker.on('message', () => {}); +}, 10); +``` + +Listeners keep piling up forever. Memory keeps growing. Eventually, process crashes. + +--- + +## **Fix** + +Instead of adding `.on()` every time, do ONE of these: + +### **1. Move the event listener outside the repeated logic** + +```js +// Only once +worker.on('message', handleMessage); + +function executeQuery() { + worker.postMessage('run'); +} +``` + +### **2. Use `.once()` if you only want listener once per job** + +```js +worker.once('message', (msg) => { + console.log(msg); +}); +``` + +### **3. Remove the listener after response** + +```js +function handler(msg) { + console.log(msg); + worker.removeListener('message', handler); +} +worker.on('message', handler); +``` + +### **4. Increase limit (not fixing root cause, just hides warning)** + +```js +worker.setMaxListeners(200); +``` + +**Not recommended** unless you really know what you're doing. + +--- + +## **Conclusion** + +You’re repeatedly registering event listeners on the same Worker instance. +Fix is to add listener once, or automatically remove it. + +### **Root cause** + +There’s a **loop** or repeated call around `executeQuery()` causing `.on("message")` to be attached many times. + +--- + +If you want, paste the code around: + +``` +executeQuery() +worker.on('message'...) +``` + +and I’ll show exactly how to fix your case. + +--- + +--- + +--- + +--- + +# Cursor plan.md + +# Fix Worker Pool Event Listener Memory Leak + +## Problem + +Each `executeQuery()` call adds a new `message` listener to the worker (line 210), which is only removed after the response arrives (line 195). With high concurrency (2 workers × 2 server instances × 3 categories), listeners accumulate beyond the 100 limit, causing performance degradation. + +## Solution + +Replace per-query listeners with a single message handler per worker that routes responses to the correct promise resolver using a Map. + +## Changes + +### 1. Add pending queries Map structure + +- Create a `Map` per worker to track in-flight queries +- `PendingQuery` interface: `{ resolve, reject, timeout, type, startTime }` +- Initialize Maps during worker creation + +### 2. Set up single message handler during initialization + +- In `initWorkers()`, after creating each worker, add a single `worker.on('message')` handler +- Handler routes responses to the correct promise based on `response.id` +- Remove the per-query listener registration from `executeQuery()` + +### 3. Refactor `executeQuery()` function + +- Register query in the worker's pending Map instead of adding a listener +- Update timeout handler to remove from Map and reject +- Remove the `worker.off()` call since we're not adding per-query listeners + +### 4. Handle cleanup + +- Update `cleanupWorkers()` to clear all pending Maps +- Ensure timeout handlers properly clean up Map entries + +## Files to modify + +- `db/emojis/emoji-worker-pool.ts` - Apply routing handler pattern +- `db/png_icons/png-worker-pool.ts` - Apply routing handler pattern +- `db/svg_icons/svg-worker-pool.ts` - Apply routing handler pattern + +## Implementation details + +- Each worker gets one persistent message listener (set during init) +- Query registration: add to Map with queryId as key +- Response handling: lookup queryId in Map, resolve/reject, remove from Map +- Timeout handling: remove from Map before rejecting +- No need to increase `setMaxListeners()` beyond default since we'll only have 1 listener per worker diff --git a/frontend/instructions.md b/frontend/md/instructions.md similarity index 100% rename from frontend/instructions.md rename to frontend/md/instructions.md diff --git a/frontend/seo.md b/frontend/md/seo.md similarity index 100% rename from frontend/seo.md rename to frontend/md/seo.md diff --git a/frontend/md/ssr_profiling.md b/frontend/md/ssr_profiling.md new file mode 100644 index 0000000000..08dce146d4 --- /dev/null +++ b/frontend/md/ssr_profiling.md @@ -0,0 +1,343 @@ +i start the server by bun run dev +But for production i do bun run build & pm2 start ecosystem.config.cjs + +now I wann track this + +This is how a typical request travels for http://localhost:4321/freedevtools/svg_icons/8bit/sharp-solid-alien-8bit/ +curl -> nginx -> pm2 -> [ process 1 entry point -> astro stuff -> db call -> astro stuff ] -> back + +I wanna profile just the things insde those [] + +you'll probably need a profile to find the root cause why astro server takes time + +Example logs as of now: +[2025-11-29T13:54:05.112Z] Request reached server: /freedevtools/svg_icons/8bit/sharp-solid-alien-8bit/ +[SVG_ICONS_DB][2025-11-29T13:54:05.122Z] Dispatching getClusterByName +[2025-11-29T13:54:05.123Z] [SVG_ICONS_DB] Worker 1 handling getClusterByName +[2025-11-29T13:54:05.123Z] [SVG_ICONS_DB] Worker 1 getClusterByName finished in 0ms +[SVG_ICONS_DB][2025-11-29T13:54:05.136Z] getClusterByName completed in 14ms +[SVG_ICONS_DB][2025-11-29T13:54:05.136Z] Dispatching getIconByUrlHash +[2025-11-29T13:54:05.137Z] [SVG_ICONS_DB] Worker 0 handling getIconByUrlHash +[2025-11-29T13:54:05.137Z] [SVG_ICONS_DB] Worker 0 getIconByUrlHash finished in 0ms +[SVG_ICONS_DB][2025-11-29T13:54:05.137Z] getIconByUrlHash completed in 1ms +[2025-11-29T13:54:05.146Z] Total request time for /freedevtools/svg_icons/8bit/sharp-solid-alien-8bit/: 34ms + +I know that DB is taking negligable amount of time to fetch the data. + +I wanna know where execatly which function what the fuck is taking rest of time i.e 30ms where is thsi big chunk going. + +Can we make use of +https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/auto-instrumentations-node#readme + +# OpenTelemetry Meta Packages for Node + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-url] + +## About + +This module provides a way to auto instrument any Node application to capture telemetry from a number of popular libraries and frameworks. +You can export the telemetry data in a variety of formats. Exporters, samplers, and more can be configured via [environment variables][env-var-url]. +The net result is the ability to gather telemetry data from a Node application without any code changes. + +This module also provides a simple way to manually initialize multiple Node instrumentations for use with the OpenTelemetry SDK. + +Compatible with OpenTelemetry JS API and SDK `2.0+`. + +## Installation + +```bash +npm install --save @opentelemetry/api +npm install --save @opentelemetry/auto-instrumentations-node +``` + +## Usage: Auto Instrumentation + +This module includes auto instrumentation for all supported instrumentations and [all available data exporters][exporter-url]. +It provides a completely automatic, out-of-the-box experience. +Please see the [Supported Instrumentations](#supported-instrumentations) section for more information. + +Enable auto instrumentation by requiring this module using the [--require flag][require-url]: + +```shell +node --require '@opentelemetry/auto-instrumentations-node/register' app.js +``` + +If your Node application is encapsulated in a complex run script, you can also set it via an environment variable before running Node. + +```shell +env NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" +``` + +The module is highly configurable using environment variables. +Many aspects of the auto instrumentation's behavior can be configured for your needs, such as resource detectors, exporter choice, exporter configuration, trace context propagation headers, and much more. +Instrumentation configuration is not yet supported through environment variables. Users that require instrumentation configuration must initialize OpenTelemetry programmatically. + +```shell +export OTEL_TRACES_EXPORTER="otlp" +export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +export OTEL_EXPORTER_OTLP_COMPRESSION="gzip" +export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://your-endpoint" +export OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-api-key" +export OTEL_EXPORTER_OTLP_TRACES_HEADERS="x-api-key=your-api-key" +export OTEL_RESOURCE_ATTRIBUTES="service.namespace=my-namespace" +export OTEL_NODE_RESOURCE_DETECTORS="env,host,os,serviceinstance" +export OTEL_SERVICE_NAME="client" +export NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" +node app.js +``` + +By default, all SDK resource detectors are used, but you can use the environment variable OTEL_NODE_RESOURCE_DETECTORS to enable only certain detectors, or completely disable them: + +- `env` +- `host` +- `os` +- `process` +- `serviceinstance` +- `container` +- `alibaba` +- `aws` +- `azure` +- `gcp` +- `all` - enable all resource detectors +- `none` - disable resource detection + +For example, to enable only the `env`, `host` detectors: + +```shell +export OTEL_NODE_RESOURCE_DETECTORS="env,host" +``` + +By default, all [Supported Instrumentations](#supported-instrumentations) are enabled, unless they are annotated with "default disabled". +You can use the environment variable `OTEL_NODE_ENABLED_INSTRUMENTATIONS` to enable only certain instrumentations, including "default disabled" ones +OR the environment variable `OTEL_NODE_DISABLED_INSTRUMENTATIONS` to disable only certain instrumentations, +by providing a comma-separated list of the instrumentation package names without the `@opentelemetry/instrumentation-` prefix. + +For example, to enable only +[@opentelemetry/instrumentation-http](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-http) +and [@opentelemetry/instrumentation-nestjs-core](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-nestjs-core) +instrumentations: + +```shell +export OTEL_NODE_ENABLED_INSTRUMENTATIONS="http,nestjs-core" +``` + +To disable only [@opentelemetry/instrumentation-net](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-net): + +```shell +export OTEL_NODE_DISABLED_INSTRUMENTATIONS="net" +``` + +If both environment variables are set, `OTEL_NODE_ENABLED_INSTRUMENTATIONS` is applied first, and then `OTEL_NODE_DISABLED_INSTRUMENTATIONS` is applied to that list. +Therefore, if the same instrumentation is included in both lists, that instrumentation will be disabled. + +To enable logging for troubleshooting, set the log level by setting the `OTEL_LOG_LEVEL` environment variable to one of the following: + +- `none` +- `error` +- `warn` +- `info` +- `debug` +- `verbose` +- `all` + +The default level is `info`. + +Notes: + +- In a production environment, it is recommended to set `OTEL_LOG_LEVEL`to `info`. +- Logs are always sent to console, no matter the environment, or debug level. +- Debug logs are extremely verbose. Enable debug logging only when needed. Debug logging negatively impacts the performance of your application. + +## Usage: Instrumentation Initialization + +OpenTelemetry Meta Packages for Node automatically loads instrumentations for Node builtin modules and common packages. + +Custom configuration for each of the instrumentations can be passed to the function, by providing an object with the name of the instrumentation as a key, and its configuration as the value. + +```javascript +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { + getNodeAutoInstrumentations, +} = require('@opentelemetry/auto-instrumentations-node'); +const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector'); +const { resourceFromAttributes } = require('@opentelemetry/resources'); +const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions'); +const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const exporter = new CollectorTraceExporter(); +const provider = new NodeTracerProvider({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'basic-service', + }), + spanProcessors: [new SimpleSpanProcessor(exporter)], +}); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + getNodeAutoInstrumentations({ + // load custom configuration for http instrumentation + '@opentelemetry/instrumentation-http': { + applyCustomAttributesOnSpan: (span) => { + span.setAttribute('foo2', 'bar2'); + }, + }, + }), + ], +}); +``` + +## Supported instrumentations + +- [@opentelemetry/instrumentation-amqplib](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-amqplib) +- [@opentelemetry/instrumentation-aws-lambda](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-aws-lambda) +- [@opentelemetry/instrumentation-aws-sdk](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-aws-sdk) +- [@opentelemetry/instrumentation-bunyan](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-bunyan) +- [@opentelemetry/instrumentation-cassandra-driver](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-cassandra-driver) +- [@opentelemetry/instrumentation-connect](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-connect) +- [@opentelemetry/instrumentation-cucumber](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-cucumber) +- [@opentelemetry/instrumentation-dataloader](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-dataloader) +- [@opentelemetry/instrumentation-dns](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-dns) +- [@opentelemetry/instrumentation-express](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-express) +- [@opentelemetry/instrumentation-fastify](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-fastify) (deprecated, default disabled) + - This component is **deprecated** in favor of the official instrumentation package [`@fastify/otel`](https://www.npmjs.com/package/@fastify/otel), maintained by the Fastify authors. + - Please see [the offical plugin's README.md](https://github.com/fastify/otel?tab=readme-ov-file#usage) for instructions on how to use `@fastify/otel`. + - This component will be removed on June 30, 2025 +- [@opentelemetry/instrumentation-fs](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-fs) (default disabled) +- [@opentelemetry/instrumentation-generic-pool](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-generic-pool) +- [@opentelemetry/instrumentation-graphql](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-graphql) +- [@opentelemetry/instrumentation-grpc](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-grpc) +- [@opentelemetry/instrumentation-hapi](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-hapi) +- [@opentelemetry/instrumentation-http](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http) +- [@opentelemetry/instrumentation-ioredis](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-ioredis) +- [@opentelemetry/instrumentation-kafkajs](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-kafkajs) +- [@opentelemetry/instrumentation-knex](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-knex) +- [@opentelemetry/instrumentation-koa](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-koa) +- [@opentelemetry/instrumentation-lru-memoizer](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-lru-memoizer) +- [@opentelemetry/instrumentation-memcached](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-memcached) +- [@opentelemetry/instrumentation-mongodb](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-mongodb) +- [@opentelemetry/instrumentation-mongoose](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-mongoose) +- [@opentelemetry/instrumentation-mysql](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-mysql) +- [@opentelemetry/instrumentation-mysql2](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-mysql2) +- [@opentelemetry/instrumentation-nestjs-core](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-nestjs-core) +- [@opentelemetry/instrumentation-net](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-net) +- [@opentelemetry/instrumentation-openai](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-openai) +- [@opentelemetry/instrumentation-oracledb](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-oracledb) +- [@opentelemetry/instrumentation-pg](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-pg) +- [@opentelemetry/instrumentation-pino](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-pino) +- [@opentelemetry/instrumentation-redis](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-redis) +- [@opentelemetry/instrumentation-restify](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-restify) +- [@opentelemetry/instrumentation-runtime-node](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-runtime-node) +- [@opentelemetry/instrumentation-socket.io](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-socket.io) +- [@opentelemetry/instrumentation-undici](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-undici) +- [@opentelemetry/instrumentation-winston](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-winston) + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: + +## License + +APACHE 2.0 - See [LICENSE][license-url] for more information. + +[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fauto-instrumentations-node.svg +[env-var-url]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration +[exporter-url]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#otlp-exporter +[require-url]: https://nodejs.org/api/cli.html#-r---require-module + +--- + +--- + +# OpenTelemetry SSR Profiling Setup + +## Overview + +Add OpenTelemetry auto-instrumentation to profile the Astro SSR request handling pipeline in production mode. This will help identify where the ~30ms overhead is occurring between DB calls and total request time. + +## Prerequisites: Jaeger Backend Setup + +**The Jaeger UI requires the backend collector to be running:** + +1. Start Jaeger all-in-one container: + +```bash +docker run --rm --name jaeger \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + cr.jaegertracing.io/jaegertracing/jaeger:2.12.0 +``` + +2. Ports: + - **16686**: Jaeger UI backend (access at http://localhost:16686) + - **4318**: OTLP HTTP receiver (where traces will be sent from the app) + - **4317**: OTLP gRPC receiver (alternative protocol) + +3. The Jaeger UI running on localhost:5173 should connect to http://localhost:16686 + +## Implementation Steps + +### 1. Install OpenTelemetry Dependencies + +Add required packages to `package.json`: + +- `@opentelemetry/api` +- `@opentelemetry/auto-instrumentations-node` +- `@opentelemetry/sdk-trace-node` +- `@opentelemetry/exporter-otlp-http` +- `@opentelemetry/resources` +- `@opentelemetry/semantic-conventions` + +### 2. Create Instrumentation Initialization File + +Create `src/instrumentation.ts` that: + +- Initializes NodeSDK with OTLP HTTP exporter +- Configures auto-instrumentations (http, express, fs if needed) +- Sets service name to "astro-ssr" +- Starts the SDK before any other code runs + +### 3. Create Production Server Wrapper + +Create `src/server-wrapper.mjs` (or similar) that: + +- Imports instrumentation first (before server entry) +- Then imports/requires the built server entry point +- This ensures instrumentation is active before Astro processes requests + +### 4. Update Production Start Script + +Modify `start-server.sh` or create `start-server-prod.sh`: + +- Change from dev mode (`astro.js dev`) to production mode +- Run the built server: `bun --max-old-space-size=16384 ./dist/server/entry.mjs` +- OR use the wrapper that loads instrumentation first +- Set environment variables for OTLP configuration: + - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318` (Jaeger collector) + - `OTEL_SERVICE_NAME=astro-ssr` + - `OTEL_LOG_LEVEL=info` + +### 5. Update PM2 Configuration (Optional) + +Update `ecosystem.config.cjs` to: + +- Use production start script instead of dev script +- Add OTLP endpoint environment variables +- Configure for production builds + +### 6. Add Manual Spans for Key Operations + +Enhance `src/middleware.ts` to: + +- Create spans for request handling +- Add attributes for route, duration, handler duration +- This provides custom instrumentation alongside auto-instrumentation + +## Files to Modify diff --git a/frontend/md/worker_conversion.md b/frontend/md/worker_conversion.md new file mode 100644 index 0000000000..d6cddcc072 --- /dev/null +++ b/frontend/md/worker_conversion.md @@ -0,0 +1,605 @@ +# Convert {CATEGORY} to Worker Pool Architecture + +## Usage Instructions + +**To use this template:** + +1. Replace all instances of `{CATEGORY}` with your category name (e.g., `emoji`, `man_pages`, `mcp`) +2. Replace `{CATEGORY_UPPER}` with uppercase version (e.g., `EMOJI`, `MAN_PAGES`, `MCP`) +3. Replace `{CATEGORY_PLURAL}` with plural form if different (e.g., `emojis`, `man-pages`, `mcps`) +4. Replace `{URL_PREFIX}` with the URL path prefix (e.g., `/freedevtools/emojis/`, `/freedevtools/man-pages/`) +5. Replace `{DB_PATH}` with database file path (e.g., `db/all_dbs/emoji-db-v1.db`) +6. Replace `{FILE_EXTENSION}` with file extension if applicable (e.g., `.svg`, `.png`, or empty string) +7. Follow each phase sequentially, checking off items as you complete them + +**Example:** To convert Emoji category: + +- `{CATEGORY}` → `emoji` +- `{CATEGORY_UPPER}` → `EMOJI` +- `{CATEGORY_PLURAL}` → `emojis` +- `{URL_PREFIX}` → `/freedevtools/emojis/` +- `{DB_PATH}` → `db/all_dbs/emoji-db-v1.db` +- `{FILE_EXTENSION}` → (empty, no extension) + +--- + +## Overview + +Convert {CATEGORY} from direct database access (`bun:sqlite`) to worker pool architecture using worker threads, matching the SVG icons implementation. This ensures consistent architecture, better performance, and scalability. + +## Prerequisites + +Before starting, verify: + +- [ ] Reference implementation exists: `db/svg_icons/` (svg-worker-pool.ts, svg-worker.ts, svg-icons-utils.ts) +- [ ] Target category has: `db/{CATEGORY}/` directory with utils file +- [ ] Target category has: `src/pages/{CATEGORY_PLURAL}/` directory +- [ ] Database exists at: `{DB_PATH}` +- [ ] Database tables have been migrated to use hash-based primary keys (if applicable) + +## ⚠️ Important: Path Resolution Fix + +**CRITICAL**: Always use `findProjectRoot()` instead of `process.cwd()` in worker-pool.ts to avoid path resolution issues when running from different directories (e.g., `dev/` subdirectory). See Phase 2, Step 3 for the implementation. This prevents "Worker file not found" errors when the server runs from a subdirectory. + +## Current State Analysis + +### SVG Icons (Reference Implementation - DO NOT MODIFY) + +- **Database Layer**: `db/svg_icons/` +- `svg-worker-pool.ts` - Worker pool manager (2 workers, round-robin) +- `svg-worker.ts` - Worker thread with SQLite queries +- `svg-icons-utils.ts` - Public API using worker pool +- `svg-icons-schema.ts` - TypeScript interfaces with `hash_name`, `url_hash`, `_json` columns + +### {CATEGORY} (Current State - TO BE CONVERTED) + +- **Database Layer**: `db/{CATEGORY}/` + - `{CATEGORY}-utils.ts` - Direct DB access using `bun:sqlite` (needs conversion) + - `{CATEGORY}-schema.ts` - May be missing hash types + - Missing: `{CATEGORY}-worker-pool.ts` + - Missing: `{CATEGORY}-worker.ts` + +### Pages + +- `src/pages/{CATEGORY_PLURAL}_pages/sitemap.xml.ts` - May use content collections (needs DB query) +- `src/pages/{CATEGORY_PLURAL}/*.astro` - May use direct DB access (needs async conversion) + +--- + +## Implementation Plan + +### Phase 1: Database Schema Updates + +**File**: `db/{CATEGORY}/{CATEGORY}-schema.ts` + +**Steps:** + +1. **Check if database uses cluster/category table with hash_name:** + + ```bash + sqlite3 {DB_PATH} "PRAGMA table_info(cluster);" + # OR + sqlite3 {DB_PATH} "PRAGMA table_info(category);" + ``` + + - If `hash_name` column exists → database is ready + - If not → run migration script first (see `db/bench/{CATEGORY}/generate-{CATEGORY}-hashes.js`) + +2. **Update TypeScript interfaces:** + - [ ] Add `hash_name: string` to main category/cluster interface (bigint stored as string) + - [ ] Add `url_hash: string` to item/icon interface if applicable (bigint stored as string) + - [ ] Update `RawClusterRow` or equivalent to use `_json` column names: + +- `keywords_json`, `tags_json`, `alternative_terms_json`, `why_choose_us_json` +- Add `hash_name: string` + - [ ] Add `preview_icons_json` related interfaces if needed: +- `PreviewIcon` interface +- `ClusterWithPreviewIcons` interface + - `RawClusterPreviewPrecomputedRow` interface + +**Note:** Database columns should already match (from migration), only TypeScript types need updating. + +**Reference:** See `db/svg_icons/svg-icons-schema.ts` for complete structure. + +--- + +### Phase 2: Create Worker Pool Infrastructure + +**File**: `db/{CATEGORY}/{CATEGORY}-worker-pool.ts` (NEW) + +**Steps:** + +1. **Copy template:** + + ```bash + cp db/svg_icons/svg-worker-pool.ts db/{CATEGORY}/{CATEGORY}-worker-pool.ts + ``` + +2. **Find and replace:** + - [ ] `SVG_ICONS_DB` → `{CATEGORY_UPPER}_DB` (all occurrences) + - [ ] `svg_icons` → `{CATEGORY}` (in paths) + - [ ] `svg-icons-db.db` → `{CATEGORY}-db.db` (in getDbPath) + - [ ] `svg-worker` → `{CATEGORY}-worker` (in worker paths) + +3. **Add findProjectRoot() function (IMPORTANT - fixes path resolution issues):** + + ```typescript + // Find project root by looking for package.json or node_modules + function findProjectRoot(): string { + let current = __dirname; + while (current !== path.dirname(current)) { + if ( + existsSync(path.join(current, 'package.json')) || + existsSync(path.join(current, 'node_modules')) + ) { + return current; + } + current = path.dirname(current); + } + // Fallback to process.cwd() if we can't find project root + return process.cwd(); + } + ``` + +4. **Update getDbPath():** + + ```typescript + function getDbPath(): string { + const projectRoot = findProjectRoot(); + return path.resolve(projectRoot, '{DB_PATH}'); + } + ``` + +5. **Update worker path resolution in initWorkers():** + + Replace `process.cwd()` with `findProjectRoot()`: + + ```typescript + // OLD (can fail if running from different directory): + const projectRoot = process.cwd(); + + // NEW (reliable path resolution): + const projectRoot = findProjectRoot(); + ``` + + This ensures the worker file is found regardless of the current working directory (e.g., when running from `dev/` subdirectory). + + **Why this matters**: If your server runs from a subdirectory (like `dev/`), `process.cwd()` will point to that subdirectory, causing the worker pool to look for files in the wrong location (e.g., `dev/dist/...` instead of `dist/...`). The `findProjectRoot()` function walks up from `__dirname` to find the actual project root. + +6. **Add max listeners (prevent memory leak warnings):** + + ```typescript + worker.setMaxListeners(100); + ``` + + Add this after creating the worker, before `worker.on('message', ...)` + +7. **Verify query interface matches:** + - [ ] Export `query` object with same methods as SVG version + - [ ] All query methods delegate to `executeQuery()` + +**Reference:** See `db/svg_icons/svg-worker-pool.ts` for complete implementation. + +--- + +### Phase 3: Create Worker Thread + +**File**: `db/{CATEGORY}/{CATEGORY}-worker.ts` (NEW) + +**Steps:** + +1. **Copy template:** + + ```bash + cp db/svg_icons/svg-worker.ts db/{CATEGORY}/{CATEGORY}-worker.ts + ``` + +2. **Find and replace:** + - [ ] `SVG_ICONS_DB` → `{CATEGORY_UPPER}_DB` (all log labels) + - [ ] Update comment: "Handles all query types for the {CATEGORY} database" + +3. **Update SQL queries:** + - [ ] Check table names match your database (may be `cluster`, `category`, `emoji`, etc.) + - [ ] Use `keywords_json`, `tags_json`, etc. if columns exist + - [ ] Use `hash_name` for category/cluster lookups + - [ ] Use `url_hash` for item lookups (if applicable) + - [ ] Include `preview_icons_json` in queries if column exists + +4. **Update URL patterns:** + - [ ] In `getIconsByCluster` or equivalent: Update URL to `{URL_PREFIX}` + - [ ] In `getIconByCategoryAndName`: Update file extension handling (`.svg` → `{FILE_EXTENSION}`) + - [ ] In transform mode: Update icon/url paths to `{URL_PREFIX}` + +5. **Update query handlers:** + - [ ] Parse JSON columns correctly (`keywords_json`, `tags_json`, etc.) + - [ ] Handle `hash_name` lookups properly + - [ ] Ensure all return types match schema interfaces + +6. **Verify all query types:** + - [ ] `getTotalIcons` / `getTotalItems` + - [ ] `getTotalClusters` / `getTotalCategories` + - [ ] `getIconsByCluster` / `getItemsByCategory` + - [ ] `getClustersWithPreviewIcons` / `getCategoriesWithPreview` + - [ ] `getClusterByName` / `getCategoryByName` + - [ ] `getClusters` / `getCategories` + - [ ] `getIconByUrlHash` / `getItemByUrlHash` + - [ ] `getIconByCategoryAndName` / `getItemByCategoryAndName` + +**Reference:** See `db/svg_icons/svg-worker.ts` for complete query implementations. + +--- + +### Phase 4: Update Utils to Use Worker Pool + +**File**: `db/{CATEGORY}/{CATEGORY}-utils.ts` + +**Steps:** + +1. **Remove direct database access:** + - [ ] Remove `import { Database } from 'bun:sqlite'` + - [ ] Remove `getDb()` function + - [ ] Remove `dbInstance` variable + +2. **Add worker pool import:** + + ```typescript + import { query } from './{CATEGORY}-worker-pool'; + ``` + +3. **Import hash utilities:** + + ```typescript + import { hashUrlToKey, hashNameToKey } from '../../src/lib/hash-utils'; + ``` + +4. **Create category-specific URL builder (if needed):** + + ```typescript + function build{CATEGORY}Url(category: string, name: string): string { + const segments = [category, name] + .filter((segment) => typeof segment === 'string' && segment.length > 0) + .map((segment) => encodeURIComponent(segment)); + return '{URL_PREFIX}' + segments.join('/'); + } + ``` + + Only needed if URL pattern differs from SVG (which uses `/` + segments). + +5. **Convert all functions to async:** + - [ ] `getTotalIcons()` → `async function getTotalIcons(): Promise` + - [ ] `getTotalClusters()` → `async function getTotalClusters(): Promise` + - [ ] `getClusters()` → `async function getClusters(): Promise` + - [ ] `getClusterByName(name: string)` → Use `hashNameToKey()` helper: + ```typescript + export async function getClusterByName( + name: string + ): Promise { + const hashName = hashNameToKey(name); + return query.getClusterByName(hashName); + } + ``` + - [ ] `getIconsByCluster()` → `async function getIconsByCluster()` + - [ ] `getClustersWithPreviewIcons()` → Use worker pool query + - [ ] `getIconByCategoryAndName()` → Use `hashUrlToKey()` and `getIconByUrlHash`: + ```typescript + export async function getIconByCategoryAndName( + category: string, + iconName: string + ): Promise { + const clusterData = await getClusterByName(category); + if (!clusterData) return null; + const filename = iconName.replace('{FILE_EXTENSION}', ''); + const url = build{CATEGORY}Url(clusterData.source_folder || category, filename); + const hashKey = hashUrlToKey(url); + return query.getIconByUrlHash(hashKey); + } + ``` + +6. **Remove all direct SQL queries:** + - [ ] Replace all `db.prepare()` calls with `query.*()` calls + - [ ] Remove all database connection code + +7. **Update return types:** + - [ ] Ensure all return types match SVG utils interface + - [ ] Add `IconWithMetadata` interface if needed + - [ ] Add `ClusterTransformed` interface if needed + +**Reference:** See `db/svg_icons/svg-icons-utils.ts` for complete structure. + +--- + +### Phase 5: Build Integration + +**File**: `integrations/copy-worker.mjs` + +**Steps:** + +1. **Locate the workers array:** + Find the `workers` array that contains SVG and PNG worker configs. + +2. **Add new worker entry:** + + ```javascript + { + source: path.join(projectRoot, 'db', '{CATEGORY}', '{CATEGORY}-worker.ts'), + dist: path.join(distDir, 'server', 'chunks', 'db', '{CATEGORY}', '{CATEGORY}-worker.js'), + name: '{CATEGORY_UPPER}', + }, + ``` + +3. **Verify:** + - [ ] Worker entry is added to the array + - [ ] Paths use correct category name + - [ ] Name is uppercase version + +**Reference:** See `integrations/copy-worker.mjs` for current structure. + +--- + +### Phase 6: Update Page Files + +#### 6.1: Update Sitemap + +**File**: `src/pages/{CATEGORY_PLURAL}_pages/sitemap.xml.ts` + +**Steps:** + +1. **Replace content collection import (if exists):** + + ```typescript + // REMOVE: + const { getCollection } = await import('astro:content'); + const entries = await getCollection('{category}Metadata'); + + // ADD: + import { getClusters } from 'db/{CATEGORY}/{CATEGORY}-utils'; + const clusters = await getClusters(); + ``` + +2. **Update pagination calculation:** + + ```typescript + const itemsPerPage = 30; + const totalPages = Math.ceil(clusters.length / itemsPerPage); + ``` + +3. **Update URL generation:** + - [ ] Use `{URL_PREFIX}` for all URLs + - [ ] Ensure pagination URLs match pattern + +**Reference:** See `src/pages/svg_icons_pages/sitemap.xml.ts` for complete example. + +#### 6.2: Update Main Pages + +**Files**: `src/pages/{CATEGORY_PLURAL}/*.astro` + +**Steps:** + +1. **Find all function calls:** + + ```bash + grep -n "getClusters\|getClusterByName\|getIconsByCluster\|getClustersWithPreviewIcons\|getTotalIcons\|getIconByCategoryAndName" src/pages/{CATEGORY_PLURAL}/*.astro + ``` + +2. **Add `await` to all async calls:** + - [ ] `getClusters()` → `await getClusters()` + - [ ] `getClusterByName()` → `await getClusterByName()` + - [ ] `getIconsByCluster()` → `await getIconsByCluster()` + - [ ] `getClustersWithPreviewIcons()` → `await getClustersWithPreviewIcons()` + - [ ] `getTotalIcons()` → `await getTotalIcons()` + - [ ] `getIconByCategoryAndName()` → `await getIconByCategoryAndName()` + +3. **Verify imports:** + - [ ] All imports from `db/{CATEGORY}/{CATEGORY}-utils` are correct + - [ ] No direct database imports remain + +4. **Check for direct DB access:** + + ```bash + grep -n "getDb()\|Database\|bun:sqlite" src/pages/{CATEGORY_PLURAL}/*.astro + ``` + + - [ ] Remove any direct database access + - [ ] Replace with worker pool calls + +**Reference:** See `src/pages/svg_icons/*.astro` for examples. + +--- + +### Phase 7: Testing & Verification + +**Checklist:** + +1. **Worker Pool Initialization:** + - [ ] Start dev server: `npm run dev` or `bun run dev` + - [ ] Check console for: `[{CATEGORY_UPPER}_DB] Initializing worker pool with 2 workers...` + - [ ] Verify: `[{CATEGORY_UPPER}_DB] Worker pool initialized in Xms` + - [ ] No errors about missing worker files + +2. **Query Functionality:** + - [ ] Test main listing page: `/{URL_PREFIX}` + - [ ] Test category page: `/{URL_PREFIX}{category}/` + - [ ] Test item page: `/{URL_PREFIX}{category}/{item}/` + - [ ] Check console logs for query execution times + - [ ] Verify no SQL errors + +3. **Build Process:** + - [ ] Run build: `npm run build` or `bun run build` + - [ ] Verify worker file compiled: `dist/server/chunks/db/{CATEGORY}/{CATEGORY}-worker.js` + - [ ] Check build logs for: `✅ Compiled {CATEGORY_UPPER} worker.js using esbuild` + +4. **Page Rendering:** + - [ ] All pages render without errors + - [ ] Data displays correctly + - [ ] Pagination works + - [ ] Sitemap generates correctly + +5. **Performance:** + - [ ] Query times are reasonable (< 100ms for simple queries) + - [ ] No memory leaks (check worker pool logs) + - [ ] Worker pool handles concurrent requests + +--- + +## Category-Specific Notes + +### For Icon Categories (SVG, PNG, etc.) + +- Use `cluster` table name +- Use `icon` table name +- URL pattern: `/freedevtools/{category}_icons/{cluster}/{icon}/` +- File extension: `.svg` or `.png` + +### For Emoji Categories + +- May use `emoji` table instead of `icon` +- May use `category` instead of `cluster` +- URL pattern: `/freedevtools/emojis/{category}/` +- No file extension in URLs +- May have different schema structure + +### For Other Categories + +- Check actual table names in database +- Check URL patterns in existing pages +- Adapt query names to match category terminology +- Verify hash column names match database structure + +--- + +## Common Issues & Solutions + +### Issue: Worker file not found + +**Error Message:** + +``` +Worker file not found. Checked: + - /path/to/project/dev/dist/server/chunks/db/{CATEGORY}/{CATEGORY}-worker.js (production) + - /path/to/project/dev/db/{CATEGORY}/{CATEGORY}-worker.ts (development) + - /path/to/project/dist/server/chunks/{CATEGORY}-worker.ts (fallback) +``` + +**Root Cause:** +The worker pool uses `process.cwd()` to resolve paths, which can be wrong if the server runs from a different directory (e.g., `dev/` subdirectory). Notice the paths include `dev/` in them, indicating `process.cwd()` is pointing to the wrong directory. + +**Solution:** + +1. **CRITICAL FIX**: Add `findProjectRoot()` function to worker-pool.ts (see Phase 2, Step 3): + + ```typescript + // Find project root by looking for package.json or node_modules + function findProjectRoot(): string { + let current = __dirname; + while (current !== path.dirname(current)) { + if ( + existsSync(path.join(current, 'package.json')) || + existsSync(path.join(current, 'node_modules')) + ) { + return current; + } + current = path.dirname(current); + } + // Fallback to process.cwd() if we can't find project root + return process.cwd(); + } + ``` + +2. **Update getDbPath()** to use `findProjectRoot()`: + + ```typescript + function getDbPath(): string { + const projectRoot = findProjectRoot(); + return path.resolve(projectRoot, '{DB_PATH}'); + } + ``` + +3. **Update initWorkers()** to use `findProjectRoot()`: + + ```typescript + // Replace this: + const projectRoot = process.cwd(); + + // With this: + const projectRoot = findProjectRoot(); + ``` + +4. **Additional checks:** + - Verify worker file exists: `db/{CATEGORY}/{CATEGORY}-worker.ts` + - Verify build integration copied file to dist: `dist/server/chunks/db/{CATEGORY}/{CATEGORY}-worker.js` + - Check the compiled worker file exists at the expected dist path + - Ensure build was run after adding the worker files + - Restart the server after making these changes + +### Issue: SQLiteError: no such column + +**Solution:** + +- Verify database schema matches TypeScript types +- Check if migration script was run +- Verify column names use `_json` suffix if applicable + +### Issue: Query timeout + +**Solution:** + +- Check if database file exists at correct path +- Verify database is not locked by another process +- Check worker pool initialization completed + +### Issue: Type errors + +**Solution:** + +- Ensure schema types match database structure +- Verify all interfaces include required fields +- Check return types match function signatures + +--- + +## Files Checklist + +**New Files to Create:** + +- [ ] `db/{CATEGORY}/{CATEGORY}-worker-pool.ts` +- [ ] `db/{CATEGORY}/{CATEGORY}-worker.ts` + +**Files to Modify:** + +- [ ] `db/{CATEGORY}/{CATEGORY}-utils.ts` (major refactor) +- [ ] `db/{CATEGORY}/{CATEGORY}-schema.ts` (add missing types) +- [ ] `integrations/copy-worker.mjs` (add worker copy) +- [ ] `src/pages/{CATEGORY_PLURAL}_pages/sitemap.xml.ts` (use DB queries) +- [ ] `src/pages/{CATEGORY_PLURAL}/*.astro` (add await, update imports) + +**Reference Files (DO NOT MODIFY):** + +- `db/svg_icons/svg-worker-pool.ts` (template) +- `db/svg_icons/svg-worker.ts` (template) +- `db/svg_icons/svg-icons-utils.ts` (template) +- `src/pages/svg_icons_pages/sitemap.xml.ts` (template) + +--- + +## Completion Checklist + +- [ ] Phase 1: Schema updated +- [ ] Phase 2: Worker pool created +- [ ] Phase 3: Worker thread created +- [ ] Phase 4: Utils refactored +- [ ] Phase 5: Build integration updated +- [ ] Phase 6: All pages updated +- [ ] Phase 7: All tests passing +- [ ] No linter errors +- [ ] Build succeeds +- [ ] Pages render correctly + +--- + +## Next Steps After Conversion + +1. Update any documentation referencing the old architecture +2. Remove any unused database connection code +3. Consider adding performance monitoring +4. Update any related scripts or tools +5. Test in production-like environment diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 0e7fd3e6d6..0000000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,14280 +0,0 @@ -{ - "name": "freedevtools-frontend", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "freedevtools-frontend", - "version": "0.0.1", - "dependencies": { - "@astrojs/react": "^4.3.0", - "@astrojs/sitemap": "^3.5.1", - "@astrojs/starlight": "^0.35.2", - "@astrojs/tailwind": "^6.0.2", - "@huggingface/transformers": "^3.7.2", - "@oneidentity/zstd-js": "^1.0.3", - "@playform/inline": "^0.1.2", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-slider": "^1.3.6", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@types/glob": "^8.1.0", - "@types/jsoneditor": "^9.9.6", - "@types/react": "^19.1.10", - "@types/react-color": "^3.0.13", - "@types/react-dom": "^19.1.7", - "@types/showdown": "^2.0.6", - "ace-builds": "^1.43.2", - "astro": "^5.6.1", - "astro-icon": "^1.1.5", - "astro-tooltips": "^0.6.2", - "beasties": "^0.3.5", - "better-sqlite3": "^9.4.3", - "class-variance-authority": "^0.7.1", - "cli-progress": "^3.12.0", - "cronstrue": "^3.3.0", - "date-fns": "^4.1.0", - "glob": "^11.0.3", - "js-tiktoken": "^1.0.21", - "jsoneditor": "^10.3.0", - "jsonrepair": "^3.13.0", - "konva": "^10.0.2", - "marked": "^16.3.0", - "purgecss": "^7.0.2", - "qrcode": "^1.5.4", - "react": "^19.1.1", - "react-color": "^2.19.3", - "react-day-picker": "^9.9.0", - "react-dom": "^19.1.1", - "react-dropzone": "^14.3.8", - "react-github-btn": "^1.4.0", - "react-konva": "^19.0.10", - "sharp": "^0.34.2", - "showdown": "^2.1.0", - "tailwind-merge": "^3.3.1" - }, - "devDependencies": { - "@tailwindcss/typography": "^0.5.19", - "@testing-library/jest-dom": "^6.7.0", - "@testing-library/react": "^16.3.0", - "@testing-library/user-event": "^14.6.1", - "@typescript-eslint/eslint-plugin": "^8.46.2", - "@typescript-eslint/parser": "^8.46.2", - "@vitejs/plugin-react": "^5.0.1", - "astro-compressor": "^1.2.0", - "axios": "^1.12.2", - "canvas": "^3.2.0", - "cheerio": "^1.1.2", - "dotenv": "^17.2.2", - "eslint": "^9.38.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-astro": "^1.3.1", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.0", - "globals": "^16.4.0", - "husky": "^9.1.7", - "jsdom": "^26.1.0", - "lint-staged": "^16.2.5", - "p-limit": "^3.1.0", - "pdfkit": "^0.17.2", - "prettier": "^3.6.2", - "prettier-plugin-astro": "^0.14.1", - "tailwindcss": "^3.4.17", - "vitest": "^3.2.4", - "xml2js": "^0.6.2" - }, - "engines": { - "node": ">=22.17.0" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@antfu/install-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", - "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", - "license": "MIT", - "dependencies": { - "package-manager-detector": "^1.3.0", - "tinyexec": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@antfu/utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", - "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "dev": true, - "license": "ISC" - }, - "node_modules/@astrojs/compiler": { - "version": "2.13.0", - "license": "MIT" - }, - "node_modules/@astrojs/internal-helpers": { - "version": "0.7.4", - "license": "MIT" - }, - "node_modules/@astrojs/markdown-remark": { - "version": "6.3.8", - "license": "MIT", - "dependencies": { - "@astrojs/internal-helpers": "0.7.4", - "@astrojs/prism": "3.3.0", - "github-slugger": "^2.0.0", - "hast-util-from-html": "^2.0.3", - "hast-util-to-text": "^4.0.2", - "import-meta-resolve": "^4.2.0", - "js-yaml": "^4.1.0", - "mdast-util-definitions": "^6.0.0", - "rehype-raw": "^7.0.0", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-smartypants": "^3.0.2", - "shiki": "^3.13.0", - "smol-toml": "^1.4.2", - "unified": "^11.0.5", - "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "unist-util-visit-parents": "^6.0.1", - "vfile": "^6.0.3" - } - }, - "node_modules/@astrojs/mdx": { - "version": "4.3.9", - "license": "MIT", - "dependencies": { - "@astrojs/markdown-remark": "6.3.8", - "@mdx-js/mdx": "^3.1.1", - "acorn": "^8.15.0", - "es-module-lexer": "^1.7.0", - "estree-util-visit": "^2.0.0", - "hast-util-to-html": "^9.0.5", - "picocolors": "^1.1.1", - "rehype-raw": "^7.0.0", - "remark-gfm": "^4.0.1", - "remark-smartypants": "^3.0.2", - "source-map": "^0.7.6", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.3" - }, - "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0" - }, - "peerDependencies": { - "astro": "^5.0.0" - } - }, - "node_modules/@astrojs/prism": { - "version": "3.3.0", - "license": "MIT", - "dependencies": { - "prismjs": "^1.30.0" - }, - "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0" - } - }, - "node_modules/@astrojs/react": { - "version": "4.4.1", - "license": "MIT", - "dependencies": { - "@vitejs/plugin-react": "^4.7.0", - "ultrahtml": "^1.6.0", - "vite": "^6.4.1" - }, - "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0" - }, - "peerDependencies": { - "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", - "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", - "react": "^17.0.2 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@astrojs/react/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "license": "MIT" - }, - "node_modules/@astrojs/react/node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@astrojs/react/node_modules/react-refresh": { - "version": "0.17.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@astrojs/react/node_modules/vite": { - "version": "6.4.1", - "license": "MIT", - "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" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "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" - }, - "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 - } - } - }, - "node_modules/@astrojs/sitemap": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "sitemap": "^8.0.0", - "stream-replace-string": "^2.0.0", - "zod": "^3.25.76" - } - }, - "node_modules/@astrojs/starlight": { - "version": "0.35.3", - "license": "MIT", - "dependencies": { - "@astrojs/markdown-remark": "^6.3.1", - "@astrojs/mdx": "^4.2.3", - "@astrojs/sitemap": "^3.3.0", - "@pagefind/default-ui": "^1.3.0", - "@types/hast": "^3.0.4", - "@types/js-yaml": "^4.0.9", - "@types/mdast": "^4.0.4", - "astro-expressive-code": "^0.41.1", - "bcp-47": "^2.1.0", - "hast-util-from-html": "^2.0.1", - "hast-util-select": "^6.0.2", - "hast-util-to-string": "^3.0.0", - "hastscript": "^9.0.0", - "i18next": "^23.11.5", - "js-yaml": "^4.1.0", - "klona": "^2.0.6", - "mdast-util-directive": "^3.0.0", - "mdast-util-to-markdown": "^2.1.0", - "mdast-util-to-string": "^4.0.0", - "pagefind": "^1.3.0", - "rehype": "^13.0.1", - "rehype-format": "^5.0.0", - "remark-directive": "^3.0.0", - "ultrahtml": "^1.6.0", - "unified": "^11.0.5", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.2" - }, - "peerDependencies": { - "astro": "^5.5.0" - } - }, - "node_modules/@astrojs/tailwind": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "autoprefixer": "^10.4.21", - "postcss": "^8.5.3", - "postcss-load-config": "^4.0.2" - }, - "peerDependencies": { - "astro": "^3.0.0 || ^4.0.0 || ^5.0.0", - "tailwindcss": "^3.0.24" - } - }, - "node_modules/@astrojs/telemetry": { - "version": "3.3.0", - "license": "MIT", - "dependencies": { - "ci-info": "^4.2.0", - "debug": "^4.4.0", - "dlv": "^1.1.3", - "dset": "^3.1.4", - "is-docker": "^3.0.0", - "is-wsl": "^3.1.0", - "which-pm-runs": "^1.1.0" - }, - "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@capsizecss/unpack": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "fontkit": "^2.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "4.2.0", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@date-fns/tz": { - "version": "1.4.1", - "license": "MIT" - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", - "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@expressive-code/core": { - "version": "0.41.3", - "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^4.0.4", - "hast-util-select": "^6.0.2", - "hast-util-to-html": "^9.0.1", - "hast-util-to-text": "^4.0.1", - "hastscript": "^9.0.0", - "postcss": "^8.4.38", - "postcss-nested": "^6.0.1", - "unist-util-visit": "^5.0.0", - "unist-util-visit-parents": "^6.0.1" - } - }, - "node_modules/@expressive-code/plugin-frames": { - "version": "0.41.3", - "license": "MIT", - "dependencies": { - "@expressive-code/core": "^0.41.3" - } - }, - "node_modules/@expressive-code/plugin-shiki": { - "version": "0.41.3", - "license": "MIT", - "dependencies": { - "@expressive-code/core": "^0.41.3", - "shiki": "^3.2.2" - } - }, - "node_modules/@expressive-code/plugin-text-markers": { - "version": "0.41.3", - "license": "MIT", - "dependencies": { - "@expressive-code/core": "^0.41.3" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "license": "MIT" - }, - "node_modules/@huggingface/jinja": { - "version": "0.5.1", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@huggingface/transformers": { - "version": "3.7.6", - "license": "Apache-2.0", - "dependencies": { - "@huggingface/jinja": "^0.5.1", - "onnxruntime-node": "1.21.0", - "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", - "sharp": "^0.34.1" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@iconify/tools": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@iconify/tools/-/tools-4.1.4.tgz", - "integrity": "sha512-s6BcNUcCxQ3S6cvhlsoWzOuBt8qKXdVyXB9rT57uSJ/ARHD7dVM43+5ERBWn3tmkMWXeJ/s9DPVc3dUasayzeA==", - "license": "MIT", - "dependencies": { - "@iconify/types": "^2.0.0", - "@iconify/utils": "^2.3.0", - "@types/tar": "^6.1.13", - "axios": "^1.12.1", - "cheerio": "1.0.0", - "domhandler": "^5.0.3", - "extract-zip": "^2.0.1", - "local-pkg": "^0.5.1", - "pathe": "^1.1.2", - "svgo": "^3.3.2", - "tar": "^6.2.1" - } - }, - "node_modules/@iconify/tools/node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=18.17" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/@iconify/tools/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@iconify/tools/node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/@iconify/tools/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/@iconify/tools/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@iconify/tools/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@iconify/tools/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "license": "MIT" - }, - "node_modules/@iconify/tools/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@iconify/tools/node_modules/undici": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", - "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/@iconify/tools/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@iconify/utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", - "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", - "license": "MIT", - "dependencies": { - "@antfu/install-pkg": "^1.0.0", - "@antfu/utils": "^8.1.0", - "@iconify/types": "^2.0.0", - "debug": "^4.4.0", - "globals": "^15.14.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.0.0", - "mlly": "^1.7.4" - } - }, - "node_modules/@iconify/utils/node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "license": "MIT" - }, - "node_modules/@iconify/utils/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@iconify/utils/node_modules/local-pkg": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@iconify/utils/node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, - "node_modules/@icons/material": { - "version": "0.2.4", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "license": "ISC", - "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/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "license": "MIT", - "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/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mdx-js/mdx": { - "version": "3.1.1", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "acorn": "^8.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-scope": "^1.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "recma-build-jsx": "^1.0.0", - "recma-jsx": "^1.0.0", - "recma-stringify": "^1.0.0", - "rehype-recma": "^1.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@oneidentity/zstd-js": { - "version": "1.0.3", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@types/emscripten": "^1.39.4" - } - }, - "node_modules/@oslojs/encoding": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/@pagefind/default-ui": { - "version": "1.4.0", - "license": "MIT" - }, - "node_modules/@pagefind/linux-x64": { - "version": "1.4.0", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@playform/inline": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@playform/inline/-/inline-0.1.2.tgz", - "integrity": "sha512-Eqk1FbKc1bNf5zyTBzrqeUvH0qExIzf6auw+yDXRl2MMZOt72FXQfnES1dtdxgjIEf4TDbPBtJ1rTofp52vDvQ==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@playform/pipe": "0.1.3", - "astro": "*", - "beasties": "0.2.0", - "deepmerge-ts": "7.1.5" - } - }, - "node_modules/@playform/inline/node_modules/beasties": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.2.0.tgz", - "integrity": "sha512-Ljqskqx/tbZagIglYoJIMzH5zgssyp+in9+9sAyh15N22AornBeIDnb8EZ6Rk+6ShfMxd92uO3gfpT0NtZbpow==", - "license": "Apache-2.0", - "dependencies": { - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "htmlparser2": "^9.1.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.49", - "postcss-media-query-parser": "^0.2.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@playform/inline/node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/@playform/pipe": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@playform/pipe/-/pipe-0.1.3.tgz", - "integrity": "sha512-cjRcaj6b8XZMS+N51In78EuD9e0x0M3gYxi2g+qUGk1iya2uxcS+aSrXxfBUZueOjxADQwpyS4zLEhlbHCGcDA==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@types/node": "22.13.14", - "deepmerge-ts": "7.1.5", - "fast-glob": "3.3.3" - } - }, - "node_modules/@playform/pipe/node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@playform/pipe/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "license": "MIT" - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "license": "BSD-3-Clause" - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-icons": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", - "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", - "license": "MIT", - "peerDependencies": { - "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.43", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "license": "MIT" - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@shikijs/core": { - "version": "3.14.0", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.14.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.5" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "3.14.0", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.14.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.3" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.14.0", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.14.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@shikijs/langs": { - "version": "3.14.0", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.14.0" - } - }, - "node_modules/@shikijs/themes": { - "version": "3.14.0", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.14.0" - } - }, - "node_modules/@shikijs/types": { - "version": "3.14.0", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "license": "MIT" - }, - "node_modules/@sphinxxxx/color-conversion": { - "version": "2.2.2", - "license": "ISC" - }, - "node_modules/@swc/helpers": { - "version": "0.5.17", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", - "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/ace": { - "version": "0.0.52", - "license": "MIT" - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/emscripten": { - "version": "1.41.5", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/fontkit": { - "version": "2.0.8", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/glob": { - "version": "8.1.0", - "license": "MIT", - "dependencies": { - "@types/minimatch": "^5.1.2", - "@types/node": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsoneditor": { - "version": "9.9.6", - "license": "MIT", - "dependencies": { - "@types/ace": "*", - "ajv": "^6.12.0" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "license": "MIT" - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/@types/nlcst": { - "version": "2.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/node": { - "version": "24.9.2", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.2", - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-color": { - "version": "3.0.13", - "license": "MIT", - "dependencies": { - "@types/reactcss": "*" - }, - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.2", - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/react-reconciler": { - "version": "0.32.2", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/reactcss": { - "version": "1.2.13", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/sax": { - "version": "1.2.7", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/showdown": { - "version": "2.0.6", - "license": "MIT" - }, - "node_modules/@types/tar": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.13.tgz", - "integrity": "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "minipass": "^4.0.0" - } - }, - "node_modules/@types/tar/node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", - "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/type-utils": "8.46.2", - "@typescript-eslint/utils": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.46.2", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", - "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", - "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", - "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", - "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", - "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/utils": "8.46.2", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", - "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.46.2", - "@typescript-eslint/tsconfig-utils": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", - "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", - "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "license": "ISC" - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.4", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.43", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/ace-builds": { - "version": "1.43.4", - "license": "BSD-3-Clause" - }, - "node_modules/acorn": { - "version": "8.15.0", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-escapes": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", - "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-iterate": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/astring": { - "version": "1.9.0", - "license": "MIT", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/astro": { - "version": "5.15.2", - "license": "MIT", - "dependencies": { - "@astrojs/compiler": "^2.12.2", - "@astrojs/internal-helpers": "0.7.4", - "@astrojs/markdown-remark": "6.3.8", - "@astrojs/telemetry": "3.3.0", - "@capsizecss/unpack": "^3.0.0", - "@oslojs/encoding": "^1.1.0", - "@rollup/pluginutils": "^5.2.0", - "acorn": "^8.15.0", - "aria-query": "^5.3.2", - "axobject-query": "^4.1.0", - "boxen": "8.0.1", - "ci-info": "^4.3.0", - "clsx": "^2.1.1", - "common-ancestor-path": "^1.0.1", - "cookie": "^1.0.2", - "cssesc": "^3.0.0", - "debug": "^4.4.1", - "deterministic-object-hash": "^2.0.2", - "devalue": "^5.3.2", - "diff": "^5.2.0", - "dlv": "^1.1.3", - "dset": "^3.1.4", - "es-module-lexer": "^1.7.0", - "esbuild": "^0.25.0", - "estree-walker": "^3.0.3", - "flattie": "^1.1.1", - "fontace": "~0.3.0", - "github-slugger": "^2.0.0", - "html-escaper": "3.0.3", - "http-cache-semantics": "^4.2.0", - "import-meta-resolve": "^4.2.0", - "js-yaml": "^4.1.0", - "magic-string": "^0.30.18", - "magicast": "^0.3.5", - "mrmime": "^2.0.1", - "neotraverse": "^0.6.18", - "p-limit": "^6.2.0", - "p-queue": "^8.1.0", - "package-manager-detector": "^1.3.0", - "picocolors": "^1.1.1", - "picomatch": "^4.0.3", - "prompts": "^2.4.2", - "rehype": "^13.0.2", - "semver": "^7.7.2", - "shiki": "^3.12.0", - "smol-toml": "^1.4.2", - "tinyexec": "^1.0.1", - "tinyglobby": "^0.2.14", - "tsconfck": "^3.1.6", - "ultrahtml": "^1.6.0", - "unifont": "~0.6.0", - "unist-util-visit": "^5.0.0", - "unstorage": "^1.17.0", - "vfile": "^6.0.3", - "vite": "^6.4.1", - "vitefu": "^1.1.1", - "xxhash-wasm": "^1.1.0", - "yargs-parser": "^21.1.1", - "yocto-spinner": "^0.2.3", - "zod": "^3.25.76", - "zod-to-json-schema": "^3.24.6", - "zod-to-ts": "^1.2.0" - }, - "bin": { - "astro": "astro.js" - }, - "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/astrodotbuild" - }, - "optionalDependencies": { - "sharp": "^0.34.0" - } - }, - "node_modules/astro-compressor": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22" - } - }, - "node_modules/astro-eslint-parser": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/astro-eslint-parser/-/astro-eslint-parser-1.2.2.tgz", - "integrity": "sha512-JepyLROIad6f44uyqMF6HKE2QbunNzp3mYKRcPoDGt0QkxXmH222FAFC64WTyQu2Kg8NNEXHTN/sWuUId9sSxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@astrojs/compiler": "^2.0.0", - "@typescript-eslint/scope-manager": "^7.0.0 || ^8.0.0", - "@typescript-eslint/types": "^7.0.0 || ^8.0.0", - "astrojs-compiler-sync": "^1.0.0", - "debug": "^4.3.4", - "entities": "^6.0.0", - "eslint-scope": "^8.0.1", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.0", - "fast-glob": "^3.3.3", - "is-glob": "^4.0.3", - "semver": "^7.3.8" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - } - }, - "node_modules/astro-eslint-parser/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/astro-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/astro-expressive-code": { - "version": "0.41.3", - "license": "MIT", - "dependencies": { - "rehype-expressive-code": "^0.41.3" - }, - "peerDependencies": { - "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" - } - }, - "node_modules/astro-icon": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/astro-icon/-/astro-icon-1.1.5.tgz", - "integrity": "sha512-CJYS5nWOw9jz4RpGWmzNQY7D0y2ZZacH7atL2K9DeJXJVaz7/5WrxeyIxO8KASk1jCM96Q4LjRx/F3R+InjJrw==", - "license": "MIT", - "dependencies": { - "@iconify/tools": "^4.0.5", - "@iconify/types": "^2.0.0", - "@iconify/utils": "^2.1.30" - } - }, - "node_modules/astro-tooltips": { - "version": "0.6.2", - "license": "ISC", - "dependencies": { - "tippy.js": "6.3.7" - } - }, - "node_modules/astro/node_modules/aria-query": { - "version": "5.3.2", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/astro/node_modules/p-limit": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/astro/node_modules/vite": { - "version": "6.4.1", - "license": "MIT", - "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" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "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" - }, - "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 - } - } - }, - "node_modules/astro/node_modules/yocto-queue": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/astrojs-compiler-sync": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/astrojs-compiler-sync/-/astrojs-compiler-sync-1.1.1.tgz", - "integrity": "sha512-0mKvB9sDQRIZPsEJadw6OaFbGJ92cJPPR++ICca9XEyiUAZqgVuk25jNmzHPT0KF80rI94trSZrUR5iHFXGGOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "synckit": "^0.11.0" - }, - "engines": { - "node": "^18.18.0 || >=20.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "@astrojs/compiler": ">=0.27.0" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "node_modules/attr-accept": { - "version": "2.2.5", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axios": { - "version": "1.13.1", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/base-64": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.21", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/bcp-47": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/bcp-47-match": { - "version": "2.0.3", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/beasties": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.5.tgz", - "integrity": "sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==", - "license": "Apache-2.0", - "dependencies": { - "css-select": "^6.0.0", - "css-what": "^7.0.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "htmlparser2": "^10.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.49", - "postcss-media-query-parser": "^0.2.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/beasties/node_modules/css-select": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", - "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^7.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "nth-check": "^2.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/beasties/node_modules/css-what": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", - "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/better-sqlite3": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz", - "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/boolean": { - "version": "3.2.0", - "license": "MIT" - }, - "node_modules/boxen": { - "version": "8.0.1", - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^8.0.0", - "chalk": "^5.3.0", - "cli-boxes": "^3.0.0", - "string-width": "^7.2.0", - "type-fest": "^4.21.0", - "widest-line": "^5.0.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brotli": { - "version": "1.3.3", - "license": "MIT", - "dependencies": { - "base64-js": "^1.1.2" - } - }, - "node_modules/browserslist": { - "version": "4.27.0", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "8.0.0", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/canvas": { - "version": "3.2.0", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.3" - }, - "engines": { - "node": "^18.12.0 || >= 20.9.0" - } - }, - "node_modules/ccount": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/check-error": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/cheerio": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "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.0.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.12.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", - "license": "BSD-2-Clause", - "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": "3.6.0", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/ci-info": { - "version": "4.3.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clone": { - "version": "2.1.2", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/collapse-white-space": { - "version": "2.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/common-ancestor-path": { - "version": "1.0.1", - "license": "ISC" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/cookie-es": { - "version": "1.2.2", - "license": "MIT" - }, - "node_modules/cronstrue": { - "version": "3.9.0", - "license": "MIT", - "bin": { - "cronstrue": "bin/cli.js" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crossws": { - "version": "0.3.5", - "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/crypto-js": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/css-select": { - "version": "5.2.2", - "license": "BSD-2-Clause", - "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-selector-parser": { - "version": "3.1.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/css-tree": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" - }, - "node_modules/cssstyle": { - "version": "4.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-urls": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/date-fns": { - "version": "4.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/date-fns-jalali": { - "version": "4.1.0-0", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "dev": true, - "license": "MIT" - }, - "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/destr": { - "version": "2.0.5", - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/deterministic-object-hash": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "base-64": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/devalue": { - "version": "5.4.2", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dfa": { - "version": "1.2.0", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "5.2.0", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/direction": { - "version": "2.0.1", - "license": "MIT", - "bin": { - "direction": "cli.js" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "license": "MIT", - "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", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "license": "BSD-2-Clause", - "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/dotenv": { - "version": "17.2.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dset": { - "version": "3.1.4", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.243", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "license": "MIT" - }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "license": "MIT" - }, - "node_modules/esast-util-from-estree": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esast-util-from-js": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "acorn": "^8.0.0", - "esast-util-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esbuild": { - "version": "0.25.11", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", - "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.0", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.5.tgz", - "integrity": "sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-astro": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-astro/-/eslint-plugin-astro-1.4.0.tgz", - "integrity": "sha512-BCHPj3gORh1RYFR3sZXY0/9N9lOOw4mQYUkMWFFoFqhZtYnytuzxlyCIr8tqMmSNdZ9P4SQNq5h4v8Amr2EE5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@jridgewell/sourcemap-codec": "^1.4.14", - "@typescript-eslint/types": "^7.7.1 || ^8", - "astro-eslint-parser": "^1.0.2", - "eslint-compat-utils": "^0.6.0", - "globals": "^16.0.0", - "postcss": "^8.4.14", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": ">=8.57.0" - } - }, - "node_modules/eslint-plugin-astro/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/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, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-attach-comments": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-scope": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-to-js": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "license": "MIT" - }, - "node_modules/expand-template": { - "version": "2.0.3", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/expect-type": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/expressive-code": { - "version": "0.41.3", - "license": "MIT", - "dependencies": { - "@expressive-code/core": "^0.41.3", - "@expressive-code/plugin-frames": "^0.41.3", - "@expressive-code/plugin-shiki": "^0.41.3", - "@expressive-code/plugin-text-markers": "^0.41.3" - } - }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/file-selector": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.7.0" - }, - "engines": { - "node": ">= 12" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatbuffers": { - "version": "25.9.23", - "license": "Apache-2.0" - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/flattie": { - "version": "1.1.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/fontace": { - "version": "0.3.1", - "license": "MIT", - "dependencies": { - "@types/fontkit": "^2.0.8", - "fontkit": "^2.0.4" - } - }, - "node_modules/fontkit": { - "version": "2.0.4", - "license": "MIT", - "dependencies": { - "@swc/helpers": "^0.5.12", - "brotli": "^1.3.2", - "clone": "^2.1.2", - "dfa": "^1.2.0", - "fast-deep-equal": "^3.1.3", - "restructure": "^3.0.0", - "tiny-inflate": "^1.0.3", - "unicode-properties": "^1.4.0", - "unicode-trie": "^2.0.0" - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/github-buttons": { - "version": "2.29.1", - "license": "BSD-2-Clause" - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "license": "MIT" - }, - "node_modules/github-slugger": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/glob": { - "version": "11.0.3", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/guid-typescript": { - "version": "1.0.9", - "license": "ISC" - }, - "node_modules/h3": { - "version": "1.15.4", - "license": "MIT", - "dependencies": { - "cookie-es": "^1.2.2", - "crossws": "^0.3.5", - "defu": "^6.1.4", - "destr": "^2.0.5", - "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.2", - "radix3": "^1.1.2", - "ufo": "^1.6.1", - "uncrypto": "^0.1.3" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-embedded": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-is-element": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-format": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-embedded": "^3.0.0", - "hast-util-minify-whitespace": "^1.0.0", - "hast-util-phrasing": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "html-whitespace-sensitive-tag-names": "^3.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html": { - "version": "2.0.3", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.1.0", - "hast-util-from-parse5": "^8.0.0", - "parse5": "^7.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-has-property": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-is-body-ok-link": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-minify-whitespace": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-embedded": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-phrasing": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-embedded": "^3.0.0", - "hast-util-has-property": "^3.0.0", - "hast-util-is-body-ok-link": "^3.0.0", - "hast-util-is-element": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-select": { - "version": "6.0.4", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "bcp-47-match": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "css-selector-parser": "^3.0.0", - "devlop": "^1.0.0", - "direction": "^2.0.0", - "hast-util-has-property": "^3.0.0", - "hast-util-to-string": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "nth-check": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-estree": { - "version": "3.1.3", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-attach-comments": "^3.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5/node_modules/property-information": { - "version": "6.5.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hast-util-to-string": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-text": { - "version": "4.0.2", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "9.0.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/html-escaper": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/html-whitespace-sensitive-tag-names": { - "version": "3.0.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/htmlparser2": { - "version": "10.0.0", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/i18next": { - "version": "23.16.8", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.4", - "license": "MIT" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/iron-webcrypto": { - "version": "1.2.1", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/its-fine": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/react-reconciler": "^0.28.9" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, - "node_modules/its-fine/node_modules/@types/react-reconciler": { - "version": "0.28.9", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/jackspeak": { - "version": "4.1.1", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/javascript-natural-sort": { - "version": "0.7.1", - "license": "MIT" - }, - "node_modules/jiti": { - "version": "1.21.7", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/jmespath": { - "version": "0.16.0", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/jpeg-exif": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/js-tiktoken": { - "version": "1.0.21", - "license": "MIT", - "dependencies": { - "base64-js": "^1.5.1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "26.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "license": "MIT" - }, - "node_modules/json-source-map": { - "version": "0.6.1", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "license": "ISC" - }, - "node_modules/json5": { - "version": "2.2.3", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsoneditor": { - "version": "10.4.2", - "license": "Apache-2.0", - "dependencies": { - "ace-builds": "^1.36.2", - "ajv": "^6.12.6", - "javascript-natural-sort": "^0.7.1", - "jmespath": "^0.16.0", - "json-source-map": "^0.6.1", - "jsonrepair": "^3.8.1", - "picomodal": "^3.0.0", - "vanilla-picker": "^2.12.3" - } - }, - "node_modules/jsonrepair": { - "version": "3.13.1", - "license": "ISC", - "bin": { - "jsonrepair": "bin/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "license": "MIT" - }, - "node_modules/konva": { - "version": "10.0.8", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/lavrton" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/konva" - }, - { - "type": "github", - "url": "https://github.com/sponsors/lavrton" - } - ], - "license": "MIT" - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/linebreak": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "base64-js": "0.0.8", - "unicode-trie": "^2.0.0" - } - }, - "node_modules/linebreak/node_modules/base64-js": { - "version": "0.0.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "license": "MIT" - }, - "node_modules/lint-staged": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz", - "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^14.0.1", - "listr2": "^9.0.5", - "micromatch": "^4.0.8", - "nano-spawn": "^2.0.0", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.8.1" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "license": "MIT", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/long": { - "version": "5.3.2", - "license": "Apache-2.0" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/markdown-extensions": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/marked": { - "version": "16.4.1", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/material-colors": { - "version": "1.2.6", - "license": "ISC" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-definitions": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-directive": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdn-data": { - "version": "2.12.2", - "license": "CC0-1.0" - }, - "node_modules/merge2": { - "version": "1.4.1", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "parse-entities": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^3.0.0", - "micromark-extension-mdx-jsx": "^3.0.0", - "micromark-extension-mdx-md": "^2.0.0", - "micromark-extension-mdxjs-esm": "^3.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.3", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.3", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "license": "MIT" - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nano-spawn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", - "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/neotraverse": { - "version": "0.6.18", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/nlcst-to-string": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/node-abi": { - "version": "3.79.0", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "license": "MIT" - }, - "node_modules/node-mock-http": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.26", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.22", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ofetch": { - "version": "1.5.0", - "license": "MIT", - "dependencies": { - "destr": "^2.0.5", - "node-fetch-native": "^1.6.7", - "ufo": "^1.6.1" - } - }, - "node_modules/ohash": { - "version": "2.0.11", - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/oniguruma-parser": { - "version": "0.12.1", - "license": "MIT" - }, - "node_modules/oniguruma-to-es": { - "version": "4.3.3", - "license": "MIT", - "dependencies": { - "oniguruma-parser": "^0.12.1", - "regex": "^6.0.1", - "regex-recursion": "^6.0.2" - } - }, - "node_modules/onnxruntime-common": { - "version": "1.21.0", - "license": "MIT" - }, - "node_modules/onnxruntime-node": { - "version": "1.21.0", - "hasInstallScript": true, - "license": "MIT", - "os": [ - "win32", - "darwin", - "linux" - ], - "dependencies": { - "global-agent": "^3.0.0", - "onnxruntime-common": "1.21.0", - "tar": "^7.0.1" - } - }, - "node_modules/onnxruntime-web": { - "version": "1.22.0-dev.20250409-89f8206ba4", - "license": "MIT", - "dependencies": { - "flatbuffers": "^25.1.24", - "guid-typescript": "^1.0.9", - "long": "^5.2.3", - "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", - "platform": "^1.3.6", - "protobufjs": "^7.2.4" - } - }, - "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { - "version": "1.22.0-dev.20250409-89f8206ba4", - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "8.1.1", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^6.1.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "6.1.4", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "license": "BlueOak-1.0.0" - }, - "node_modules/package-manager-detector": { - "version": "1.5.0", - "license": "MIT" - }, - "node_modules/pagefind": { - "version": "1.4.0", - "license": "MIT", - "bin": { - "pagefind": "lib/runner/bin.cjs" - }, - "optionalDependencies": { - "@pagefind/darwin-arm64": "1.4.0", - "@pagefind/darwin-x64": "1.4.0", - "@pagefind/freebsd-x64": "1.4.0", - "@pagefind/linux-arm64": "1.4.0", - "@pagefind/linux-x64": "1.4.0", - "@pagefind/windows-x64": "1.4.0" - } - }, - "node_modules/pako": { - "version": "0.2.9", - "license": "MIT" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "license": "MIT" - }, - "node_modules/parse-latin": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "@types/unist": "^3.0.0", - "nlcst-to-string": "^4.0.0", - "unist-util-modify-children": "^4.0.0", - "unist-util-visit-children": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "license": "MIT", - "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", - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.0", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.2", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/pdfkit": { - "version": "0.17.2", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-js": "^4.2.0", - "fontkit": "^2.0.4", - "jpeg-exif": "^1.1.4", - "linebreak": "^1.1.0", - "png-js": "^1.0.0" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/picomodal": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/platform": { - "version": "1.3.6", - "license": "MIT" - }, - "node_modules/png-js": { - "version": "1.0.0", - "dev": true - }, - "node_modules/pngjs": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-import/node_modules/resolve": { - "version": "1.22.11", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "license": "MIT" - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "license": "MIT", - "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/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.6.2", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-astro": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", - "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@astrojs/compiler": "^2.9.1", - "prettier": "^3.0.0", - "sass-formatter": "^0.7.6" - }, - "engines": { - "node": "^14.15.0 || >=16.0.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/prismjs": { - "version": "1.30.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, - "node_modules/property-information": { - "version": "7.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/purgecss": { - "version": "7.0.2", - "license": "MIT", - "dependencies": { - "commander": "^12.1.0", - "glob": "^11.0.0", - "postcss": "^8.4.47", - "postcss-selector-parser": "^6.1.2" - }, - "bin": { - "purgecss": "bin/purgecss.js" - } - }, - "node_modules/purgecss/node_modules/commander": { - "version": "12.1.0", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/purgecss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/qrcode": { - "version": "1.5.4", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/radix3": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/rc": { - "version": "1.2.8", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "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/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "19.2.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-color": { - "version": "2.19.3", - "license": "MIT", - "dependencies": { - "@icons/material": "^0.2.4", - "lodash": "^4.17.15", - "lodash-es": "^4.17.15", - "material-colors": "^1.2.1", - "prop-types": "^15.5.10", - "reactcss": "^1.2.0", - "tinycolor2": "^1.4.1" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-day-picker": { - "version": "9.11.1", - "license": "MIT", - "dependencies": { - "@date-fns/tz": "^1.4.1", - "date-fns": "^4.1.0", - "date-fns-jalali": "^4.1.0-0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/gpbl" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.0", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.0" - } - }, - "node_modules/react-dropzone": { - "version": "14.3.8", - "license": "MIT", - "dependencies": { - "attr-accept": "^2.2.4", - "file-selector": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "react": ">= 16.8 || 18.0.0" - } - }, - "node_modules/react-github-btn": { - "version": "1.4.0", - "license": "BSD-2-Clause", - "dependencies": { - "github-buttons": "^2.22.0" - }, - "peerDependencies": { - "react": ">=16.3.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/react-konva": { - "version": "19.2.0", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/lavrton" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/konva" - }, - { - "type": "github", - "url": "https://github.com/sponsors/lavrton" - } - ], - "license": "MIT", - "dependencies": { - "@types/react-reconciler": "^0.32.2", - "its-fine": "^2.0.0", - "react-reconciler": "0.33.0", - "scheduler": "0.27.0" - }, - "peerDependencies": { - "konva": "^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0", - "react": "^19.2.0", - "react-dom": "^19.2.0" - } - }, - "node_modules/react-reconciler": { - "version": "0.33.0", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^19.2.0" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/reactcss": { - "version": "1.2.3", - "license": "MIT", - "dependencies": { - "lodash": "^4.0.1" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/recma-build-jsx": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-build-jsx": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-jsx": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "acorn-jsx": "^5.0.0", - "estree-util-to-js": "^2.0.0", - "recma-parse": "^1.0.0", - "recma-stringify": "^1.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/recma-parse": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "esast-util-from-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-stringify": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-to-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regex": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-recursion": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "license": "MIT" - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rehype": { - "version": "13.0.2", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "rehype-parse": "^9.0.0", - "rehype-stringify": "^10.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-expressive-code": { - "version": "0.41.3", - "license": "MIT", - "dependencies": { - "expressive-code": "^0.41.3" - } - }, - "node_modules/rehype-format": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-format": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-parse": { - "version": "9.0.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-html": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-raw": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-recma": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "hast-util-to-estree": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-stringify": { - "version": "10.0.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-html": "^9.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-directive": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-directive": "^3.0.0", - "micromark-extension-directive": "^3.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "3.1.1", - "license": "MIT", - "dependencies": { - "mdast-util-mdx": "^3.0.0", - "micromark-extension-mdxjs": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-smartypants": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "retext": "^9.0.0", - "retext-smartypants": "^6.0.0", - "unified": "^11.0.4", - "unist-util-visit": "^5.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restructure": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/retext": { - "version": "9.0.0", - "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "retext-latin": "^4.0.0", - "retext-stringify": "^4.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/retext-latin": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "parse-latin": "^7.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/retext-smartypants": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "nlcst-to-string": "^4.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/retext-stringify": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "nlcst-to-string": "^4.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/roarr": { - "version": "2.15.4", - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/rollup": { - "version": "4.52.5", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", - "fsevents": "~2.3.2" - } - }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "dev": true, - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/s.color": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", - "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/sass-formatter": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz", - "integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "suf-log": "^2.5.3" - } - }, - "node_modules/sax": { - "version": "1.4.1", - "license": "ISC" - }, - "node_modules/saxes": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "license": "MIT", - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sharp": { - "version": "0.34.4", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shiki": { - "version": "3.14.0", - "license": "MIT", - "dependencies": { - "@shikijs/core": "3.14.0", - "@shikijs/engine-javascript": "3.14.0", - "@shikijs/engine-oniguruma": "3.14.0", - "@shikijs/langs": "3.14.0", - "@shikijs/themes": "3.14.0", - "@shikijs/types": "3.14.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/showdown": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "commander": "^9.0.0" - }, - "bin": { - "showdown": "bin/showdown.js" - }, - "funding": { - "type": "individual", - "url": "https://www.paypal.me/tiviesantos" - } - }, - "node_modules/showdown/node_modules/commander": { - "version": "9.5.0", - "license": "MIT", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "license": "MIT" - }, - "node_modules/sitemap": { - "version": "8.0.2", - "license": "MIT", - "dependencies": { - "@types/node": "^17.0.5", - "@types/sax": "^1.2.1", - "arg": "^5.0.0", - "sax": "^1.4.1" - }, - "bin": { - "sitemap": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" - } - }, - "node_modules/sitemap/node_modules/@types/node": { - "version": "17.0.45", - "license": "MIT" - }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/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, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/smol-toml": { - "version": "1.4.2", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" - }, - "funding": { - "url": "https://github.com/sponsors/cyyynthia" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "license": "BSD-3-Clause" - }, - "node_modules/stackback": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "dev": true, - "license": "MIT" - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-replace-string": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "license": "MIT", - "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/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "10.6.0", - "license": "MIT" - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "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", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/style-to-js": { - "version": "1.1.18", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.11" - } - }, - "node_modules/style-to-object": { - "version": "1.0.11", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.4" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "license": "ISC", - "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/sucrase/node_modules/jackspeak": { - "version": "3.4.3", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/sucrase/node_modules/lru-cache": { - "version": "10.4.3", - "license": "ISC" - }, - "node_modules/sucrase/node_modules/path-scurry": { - "version": "1.11.1", - "license": "BlueOak-1.0.0", - "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/suf-log": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", - "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", - "dev": true, - "license": "MIT", - "dependencies": { - "s.color": "0.0.15" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "license": "MIT", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/svgo/node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/svgo/node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0" - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/tailwind-merge": { - "version": "3.3.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.18", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tailwindcss/node_modules/resolve": { - "version": "1.22.11", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tar": { - "version": "7.5.1", - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "license": "MIT", - "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/tar/node_modules/yallist": { - "version": "5.0.0", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-inflate": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tippy.js": { - "version": "6.3.7", - "license": "MIT", - "dependencies": { - "@popperjs/core": "^2.9.0" - } - }, - "node_modules/tldts": { - "version": "6.1.86", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "license": "Apache-2.0" - }, - "node_modules/tsconfck": { - "version": "3.1.6", - "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", - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "license": "MIT" - }, - "node_modules/ultrahtml": { - "version": "1.6.0", - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/uncrypto": { - "version": "0.1.3", - "license": "MIT" - }, - "node_modules/undici": { - "version": "7.16.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "license": "MIT" - }, - "node_modules/unicode-properties": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.0", - "unicode-trie": "^2.0.0" - } - }, - "node_modules/unicode-trie": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "pako": "^0.2.5", - "tiny-inflate": "^1.0.0" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unifont": { - "version": "0.6.0", - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0", - "ofetch": "^1.4.1", - "ohash": "^2.0.0" - } - }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-modify-children": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "array-iterate": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-children": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unstorage": { - "version": "1.17.1", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^4.0.3", - "destr": "^2.0.5", - "h3": "^1.15.4", - "lru-cache": "^10.4.3", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.4.1", - "ufo": "^1.6.1" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6.0.3 || ^7.0.0", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1.0.1", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } - } - }, - "node_modules/unstorage/node_modules/chokidar": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/unstorage/node_modules/lru-cache": { - "version": "10.4.3", - "license": "ISC" - }, - "node_modules/unstorage/node_modules/readdirp": { - "version": "4.1.2", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.4", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/vanilla-picker": { - "version": "2.12.3", - "license": "ISC", - "dependencies": { - "@sphinxxxx/color-conversion": "^2.2.2" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "7.1.12", - "devOptional": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "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 - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "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" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitefu": { - "version": "1.1.1", - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "0.3.2", - "dev": true, - "license": "MIT" - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "license": "ISC" - }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/widest-line": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "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", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "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", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xml2js": { - "version": "0.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/xxhash-wasm": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/y18n": { - "version": "4.0.3", - "license": "ISC" - }, - "node_modules/yallist": { - "version": "3.1.1", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.1", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yargs": { - "version": "15.4.1", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/camelcase": { - "version": "5.3.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/yargs/node_modules/find-up": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "18.1.3", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yocto-spinner": { - "version": "0.2.3", - "license": "MIT", - "dependencies": { - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18.19" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - }, - "node_modules/zod-to-ts": { - "version": "1.2.0", - "peerDependencies": { - "typescript": "^4.9.4 || ^5.0.2", - "zod": "^3" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json index 14871648c1..eba6272c69 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,24 +7,29 @@ "node": ">=22.17.0" }, "scripts": { - "dev": "node --max-old-space-size=16384 ./node_modules/astro/astro.js dev", + "dev": "bun --max-old-space-size=16384 ./node_modules/astro/astro.js dev", + "dev:light": "bun --max-old-space-size=4096 ./node_modules/astro/astro.js dev", "start": "astro dev", - "build": "node --max-old-space-size=16384 ./node_modules/astro/astro.js build", - "build:mcp": "node scripts/build-mcp.js", - "build:tldr": "node scripts/builds/tldr.js", - "build:icons": "node scripts/builds/icons.js", - "build:index": "node scripts/builds/index.js", - "build:man-pages": "node scripts/builds/man-pages.js", + "build": "bun run ./node_modules/astro/astro.js build", + "build:mcp": "bun scripts/builds/mcp.js", + "build:tldr": "bun scripts/builds/tldr.js", + "build:icons": "bun scripts/builds/icons.js", + "build:emojis": "bun scripts/builds/emojis.js", + "build:man-pages": "bun scripts/builds/man-pages.js", + "build:cheatsheets": "bun scripts/builds/cheatsheets.js", + "serve-ssr": "bun run ./dist/server/entry.mjs", + "build:index": "bun scripts/builds/index.js", "preview": "astro preview", - "astro": "astro", "format": "prettier --write .", - "banner:generate": "node scripts/tool-banners/create-tool-banner.cjs generate", - "pagespeed": "node scripts/pageSpeed.cjs", - "pagespeed:all": "node scripts/pageSpeed.cjs --all", - "pagespeed:minimal": "node scripts/pageSpeed.cjs --minimal", - "pagespeed:all:minimal": "node scripts/pageSpeed.cjs --all --minimal" + "banner:generate": "bun run scripts/tool-banners/create-tool-banner.cjs generate", + "pagespeed": "bun run scripts/pageSpeed.cjs", + "pagespeed:all": "bun run scripts/pageSpeed.cjs --all", + "pagespeed:minimal": "bun run scripts/pageSpeed.cjs --minimal", + "pagespeed:all:minimal": "bun run scripts/pageSpeed.cjs --all --minimal", + "test": "bun test" }, "dependencies": { + "@astrojs/node": "^9.5.0", "@astrojs/react": "^4.3.0", "@astrojs/sitemap": "^3.5.1", "@astrojs/starlight": "^0.35.2", @@ -56,7 +61,6 @@ "astro-icon": "^1.1.5", "astro-tooltips": "^0.6.2", "beasties": "^0.3.5", - "better-sqlite3": "^9.4.3", "class-variance-authority": "^0.7.1", "cli-progress": "^3.12.0", "cronstrue": "^3.3.0", @@ -67,6 +71,7 @@ "jsonrepair": "^3.13.0", "konva": "^10.0.2", "marked": "^16.3.0", + "onnxruntime-node": "^1.23.2", "purgecss": "^7.0.2", "qrcode": "^1.5.4", "react": "^19.1.1", @@ -78,6 +83,7 @@ "react-konva": "^19.0.10", "sharp": "^0.34.2", "showdown": "^2.1.0", + "sqlite3": "https://github.com/tryghost/node-sqlite3/tarball/master", "tailwind-merge": "^3.3.1" }, "devDependencies": { @@ -85,6 +91,7 @@ "@testing-library/jest-dom": "^6.7.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/bun": "^1.3.3", "@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/parser": "^8.46.2", "@vitejs/plugin-react": "^5.0.1", @@ -114,10 +121,10 @@ "lint-staged": { "*.{js,jsx,ts,tsx,astro}": [ "prettier --write", - "eslint --fix --max-warnings 0" + "eslint --fix" ], "*.{json,css,md}": [ "prettier --write" ] } -} +} \ No newline at end of file diff --git a/frontend/scripts/builds/cheatsheets.js b/frontend/scripts/builds/cheatsheets.js new file mode 100644 index 0000000000..9344dfdc1b --- /dev/null +++ b/frontend/scripts/builds/cheatsheets.js @@ -0,0 +1,189 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m' +}; + +function log(message, color = 'white') { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function logStep(step, message) { + log(`\n🔧 ${step}`, 'cyan'); + log(message, 'white'); +} + +function logSuccess(message) { + log(`✅ ${message}`, 'green'); +} + +function logError(message) { + log(`❌ ${message}`, 'red'); +} + +function logInfo(message) { + log(`ℹ️ ${message}`, 'blue'); +} + +function logWarning(message) { + log(`⚠️ ${message}`, 'yellow'); +} + +// Get the project root directory (frontend folder) +const projectRoot = path.resolve(__dirname, '../..'); +const pagesDir = path.join(projectRoot, 'src', 'pages'); + +function excludeUnchangedSections() { + logStep('Excluding unchanged sections from build', 'Building only cheatsheets section...'); + + const changedSections = ['c']; // Only build cheatsheets (c directory) + logInfo(`Building strategy: ${changedSections.join(' ')}`); + + logInfo('🎯 Selective build mode - cheatsheets only'); + + // Get all directories in pages folder + const dirs = fs.readdirSync(pagesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + + for (const dir of dirs) { + const dirPath = path.join(pagesDir, dir); + const dirName = dir; + + // Only include cheatsheets (c), exclude everything else (including already excluded directories) + if (changedSections.includes(dirName)) { + logSuccess(`Including: ${dirName}`); + } else { + const excludedDirName = `_${dirName}`; + const excludedDirPath = path.join(pagesDir, excludedDirName); + + try { + fs.renameSync(dirPath, excludedDirPath); + logWarning(`Excluding: ${dirName} -> ${excludedDirName}`); + } catch (error) { + logError(`Failed to exclude ${dirName}: ${error.message}`); + } + } + } +} + +function restoreOriginalStructure() { + logStep('Restoring original folder structure', 'Moving excluded directories back...'); + + const dirs = fs.readdirSync(pagesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory() && dirent.name.startsWith('_')) + .map(dirent => dirent.name); + + for (const dir of dirs) { + const originalName = dir.substring(1); // Remove the underscore + const currentPath = path.join(pagesDir, dir); + const originalPath = path.join(pagesDir, originalName); + + try { + fs.renameSync(currentPath, originalPath); + logSuccess(`Restored ${dir} to ${originalName}`); + } catch (error) { + logError(`Failed to restore ${dir}: ${error.message}`); + } + } +} + +function buildProject() { + logStep('Building project', 'Running Astro build for cheatsheets section only...'); + + try { + execSync('bun run ./node_modules/astro/astro.js build', { + cwd: projectRoot, + stdio: 'inherit', + env: { + ...process.env, + NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + UV_THREADPOOL_SIZE: '64', // 4x cores for I/O operations + NODE_OPTIONS_EXTRA: '--experimental-loader' // Enable experimental features for better performance + } + }); + logSuccess('Build completed successfully'); + } catch (error) { + logError(`Build failed: ${error.message}`); + throw error; + } +} + +function showBuildInfo() { + log('\n📦 cheatsheets Build Script', 'magenta'); + log('==================', 'magenta'); + log('This script will build only the cheatsheets section (c/) by excluding all other page directories.', 'white'); + log('The excluded directories will be temporarily renamed with an underscore prefix.', 'white'); + log('After the build, the original structure will be restored.\n', 'white'); +} + +async function main() { + try { + showBuildInfo(); + + // Check if we're in the right directory + if (!fs.existsSync(pagesDir)) { + logError(`Pages directory not found: ${pagesDir}`); + process.exit(1); + } + + // Step 1: Exclude unchanged sections + excludeUnchangedSections(); + + // Step 2: Build project + buildProject(); + + logSuccess('\n🎉 cheatsheets build completed successfully!'); + logInfo('The build output is in the dist/ directory'); + + } catch (error) { + logError(`\n💥 Build failed: ${error.message}`); + process.exit(1); + } finally { + // Always restore the original structure, even if build fails + try { + restoreOriginalStructure(); + } catch (restoreError) { + logError(`Failed to restore original structure: ${restoreError.message}`); + } + } +} + +// Handle cleanup on process termination +process.on('SIGINT', () => { + logWarning('\n⚠️ Build interrupted. Restoring original structure...'); + try { + restoreOriginalStructure(); + } catch (error) { + logError(`Failed to restore: ${error.message}`); + } + process.exit(1); +}); + +process.on('SIGTERM', () => { + logWarning('\n⚠️ Build terminated. Restoring original structure...'); + try { + restoreOriginalStructure(); + } catch (error) { + logError(`Failed to restore: ${error.message}`); + } + process.exit(1); +}); + +main(); diff --git a/frontend/scripts/builds/emojis.js b/frontend/scripts/builds/emojis.js new file mode 100644 index 0000000000..6613e9bba7 --- /dev/null +++ b/frontend/scripts/builds/emojis.js @@ -0,0 +1,210 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m' +}; + +function log(message, color = 'white') { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function logStep(step, message) { + log(`\n🔧 ${step}`, 'cyan'); + log(message, 'white'); +} + +function logSuccess(message) { + log(`✅ ${message}`, 'green'); +} + +function logError(message) { + log(`❌ ${message}`, 'red'); +} + +function logInfo(message) { + log(`ℹ️ ${message}`, 'blue'); +} + +function logWarning(message) { + log(`⚠️ ${message}`, 'yellow'); +} + +// Get the project root directory (frontend folder) +const projectRoot = path.resolve(__dirname, '../..'); +const pagesDir = path.join(projectRoot, 'src', 'pages'); + +function excludeUnchangedSections() { + logStep('Excluding unchanged sections from build', 'Building only emojis section...'); + + const changedSections = ['emojis']; // Only build emojis + logInfo(`Building strategy: ${changedSections.join(' ')}`); + + logInfo('🎯 Selective build mode - emojis only'); + + // Get all directories in pages folder + const dirs = fs.readdirSync(pagesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + + for (const dir of dirs) { + const dirPath = path.join(pagesDir, dir); + const dirName = dir; + + // Only include emojis, exclude everything else (including already excluded directories) + if (changedSections.includes(dirName)) { + logSuccess(`Including: ${dirName}`); + } else { + const excludedDirName = `_${dirName}`; + const excludedDirPath = path.join(pagesDir, excludedDirName); + + try { + fs.renameSync(dirPath, excludedDirPath); + logWarning(`Excluding: ${dirName} -> ${excludedDirName}`); + } catch (error) { + logError(`Failed to exclude ${dirName}: ${error.message}`); + } + } + } +} + +function restoreOriginalStructure() { + logStep('Restoring original folder structure', 'Moving excluded directories back...'); + + const dirs = fs.readdirSync(pagesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory() && dirent.name.startsWith('_')) + .map(dirent => dirent.name); + + for (const dir of dirs) { + const originalName = dir.substring(1); // Remove the underscore + const currentPath = path.join(pagesDir, dir); + const originalPath = path.join(pagesDir, originalName); + + try { + fs.renameSync(currentPath, originalPath); + logSuccess(`Restored ${dir} to ${originalName}`); + } catch (error) { + logError(`Failed to restore ${dir}: ${error.message}`); + } + } +} + +function installDependencies() { + logStep('Installing dependencies', 'Running npm install...'); + + try { + execSync('npm install', { + cwd: projectRoot, + stdio: 'inherit', + env: { + ...process.env, + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system + UV_THREADPOOL_SIZE: '64' // 4x cores for I/O operations + } + }); + logSuccess('Dependencies installed successfully'); + } catch (error) { + logError(`Failed to install dependencies: ${error.message}`); + throw error; + } +} + +function buildProject() { + logStep('Building project', 'Running Astro build for emojis section only...'); + + try { + execSync('npx astro build', { + cwd: projectRoot, + stdio: 'inherit', + env: { + ...process.env, + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system + UV_THREADPOOL_SIZE: '64', // 4x cores for I/O operations + NODE_OPTIONS_EXTRA: '--experimental-loader' // Enable experimental features for better performance + } + }); + logSuccess('Build completed successfully'); + } catch (error) { + logError(`Build failed: ${error.message}`); + throw error; + } +} + +function showBuildInfo() { + log('\n📦 emojis Build Script', 'magenta'); + log('==================', 'magenta'); + log('This script will build only the emojis section by excluding all other page directories.', 'white'); + log('The excluded directories will be temporarily renamed with an underscore prefix.', 'white'); + log('After the build, the original structure will be restored.\n', 'white'); +} + +async function main() { + try { + showBuildInfo(); + + // Check if we're in the right directory + if (!fs.existsSync(pagesDir)) { + logError(`Pages directory not found: ${pagesDir}`); + process.exit(1); + } + + // Step 1: Exclude unchanged sections + excludeUnchangedSections(); + + + // Step 3: Build project + buildProject(); + + logSuccess('\n🎉 emojis build completed successfully!'); + logInfo('The build output is in the dist/ directory'); + + } catch (error) { + logError(`\n💥 Build failed: ${error.message}`); + process.exit(1); + } finally { + // Always restore the original structure, even if build fails + try { + restoreOriginalStructure(); + } catch (restoreError) { + logError(`Failed to restore original structure: ${restoreError.message}`); + } + } +} + +// Handle cleanup on process termination +process.on('SIGINT', () => { + logWarning('\n⚠️ Build interrupted. Restoring original structure...'); + try { + restoreOriginalStructure(); + } catch (error) { + logError(`Failed to restore: ${error.message}`); + } + process.exit(1); +}); + +process.on('SIGTERM', () => { + logWarning('\n⚠️ Build terminated. Restoring original structure...'); + try { + restoreOriginalStructure(); + } catch (error) { + logError(`Failed to restore: ${error.message}`); + } + process.exit(1); +}); + +main(); diff --git a/frontend/scripts/builds/icons.js b/frontend/scripts/builds/icons.js index d726fcea5a..2c790f562a 100644 --- a/frontend/scripts/builds/icons.js +++ b/frontend/scripts/builds/icons.js @@ -52,13 +52,13 @@ const pagesDir = path.join(projectRoot, 'src', 'pages'); function excludeUnchangedSections() { logStep( 'Excluding unchanged sections from build', - 'Building only Icons sections...' + 'Building only SVG Icons section...' ); - const changedSections = ['svg_icons', 'png_icons']; // Only build Icons + const changedSections = ['svg_icons']; // Only build SVG Icons logInfo(`Building strategy: ${changedSections.join(' ')}`); - logInfo('🎯 Selective build mode - Icons only'); + logInfo('🎯 Selective build mode - SVG Icons only'); // Get all directories in pages folder const dirs = fs @@ -113,7 +113,10 @@ function restoreOriginalStructure() { } function buildProject() { - logStep('Building project', 'Running Astro build for Icons sections only...'); + logStep( + 'Building project', + 'Running Astro build for SVG Icons section only...' + ); try { execSync('npx astro build', { @@ -121,7 +124,7 @@ function buildProject() { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system UV_THREADPOOL_SIZE: '64', // 4x cores for I/O operations NODE_OPTIONS_EXTRA: '--experimental-loader', // Enable experimental features for better performance }, @@ -134,10 +137,10 @@ function buildProject() { } function showBuildInfo() { - log('\n📦 Icons Build Script', 'magenta'); - log('====================', 'magenta'); + log('\n📦 SVG Icons Build Script', 'magenta'); + log('========================', 'magenta'); log( - 'This script will build only the Icons sections (SVG & PNG) by excluding all other page directories.', + 'This script will build only the SVG Icons section by excluding all other page directories.', 'white' ); log( @@ -163,7 +166,7 @@ async function main() { // Step 2: Build project buildProject(); - logSuccess('\n🎉 Icons build completed successfully!'); + logSuccess('\n🎉 SVG Icons build completed successfully!'); logInfo('The build output is in the dist/ directory'); } catch (error) { logError(`\n💥 Build failed: ${error.message}`); diff --git a/frontend/scripts/builds/index.js b/frontend/scripts/builds/index.js index 0e447812c6..01c8d4bd63 100755 --- a/frontend/scripts/builds/index.js +++ b/frontend/scripts/builds/index.js @@ -112,7 +112,7 @@ function buildProject() { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system UV_THREADPOOL_SIZE: '64', // 4x cores for I/O operations NODE_OPTIONS_EXTRA: '--experimental-loader', // Enable experimental features for better performance }, diff --git a/frontend/scripts/builds/man-pages.js b/frontend/scripts/builds/man-pages.js old mode 100644 new mode 100755 index ac0987bdaa..16eeb9bbea --- a/frontend/scripts/builds/man-pages.js +++ b/frontend/scripts/builds/man-pages.js @@ -113,7 +113,7 @@ function installDependencies() { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system UV_THREADPOOL_SIZE: '64' // 4x cores for I/O operations } }); @@ -133,7 +133,7 @@ function buildProject() { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system UV_THREADPOOL_SIZE: '64', // 4x cores for I/O operations NODE_OPTIONS_EXTRA: '--experimental-loader' // Enable experimental features for better performance } @@ -208,3 +208,4 @@ process.on('SIGTERM', () => { }); main(); + diff --git a/frontend/scripts/builds/mcp.js b/frontend/scripts/builds/mcp.js index e51abe1695..1cc8d08c6e 100644 --- a/frontend/scripts/builds/mcp.js +++ b/frontend/scripts/builds/mcp.js @@ -113,7 +113,7 @@ function installDependencies() { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system UV_THREADPOOL_SIZE: '64' // 4x cores for I/O operations } }); @@ -128,12 +128,12 @@ function buildProject() { logStep('Building project', 'Running Astro build for MCP section only...'); try { - execSync('npx astro build', { + execSync('bun run ./node_modules/astro/astro.js build', { cwd: projectRoot, stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system UV_THREADPOOL_SIZE: '64', // 4x cores for I/O operations NODE_OPTIONS_EXTRA: '--experimental-loader' // Enable experimental features for better performance } diff --git a/frontend/scripts/builds/tldr.js b/frontend/scripts/builds/tldr.js index 888724b2a8..143efda4a5 100644 --- a/frontend/scripts/builds/tldr.js +++ b/frontend/scripts/builds/tldr.js @@ -113,7 +113,7 @@ function installDependencies() { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system UV_THREADPOOL_SIZE: '64' // 4x cores for I/O operations } }); @@ -133,7 +133,7 @@ function buildProject() { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + NODE_OPTIONS: '--max-old-space-size=8192', // 8GB for 8GB system UV_THREADPOOL_SIZE: '64', // 4x cores for I/O operations NODE_OPTIONS_EXTRA: '--experimental-loader' // Enable experimental features for better performance } diff --git a/frontend/scripts/cheatsheets/build_cheatsheets_db.py b/frontend/scripts/cheatsheets/build_cheatsheets_db.py new file mode 100644 index 0000000000..e22497003e --- /dev/null +++ b/frontend/scripts/cheatsheets/build_cheatsheets_db.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +""" +Build a SQLite database from HTML cheatsheets. + +- Scans data/cheatsheets/**/*.html +- Creates SQLite DB at db/all_dbs/cheatsheets-db.db +- Table: cheatsheet(id, category, name, slug, content, title, description, keywords) +- Table: category(id, name, slug, description, keywords, features) +- Table: overview(id, total_count) +""" + +import sqlite3 +import re +import json +import hashlib +from pathlib import Path + +BASE_DIR = Path(__file__).parent.parent.parent +DATA_DIR = BASE_DIR / "data" / "cheatsheets" +DB_PATH = BASE_DIR / "db" / "all_dbs" / "cheatsheets-db.db" + +def generate_hash_id(category: str, slug: str) -> int: + """ + Generate a 64-bit signed integer hash from category and slug. + Matches the behavior of crypto.createHash('sha256').update(url).digest().readBigInt64BE(0) + """ + combined = f"{category}{slug}" + hash_bytes = hashlib.sha256(combined.encode('utf-8')).digest() + # Take first 8 bytes, interpret as big-endian signed integer + return int.from_bytes(hash_bytes[:8], byteorder='big', signed=True) + +def ensure_schema(conn: sqlite3.Connection) -> None: + cur = conn.cursor() + + # Performance optimizations + cur.execute("PRAGMA journal_mode = WAL;") + cur.execute("PRAGMA synchronous = OFF;") + cur.execute("PRAGMA cache_size = -128000;") + cur.execute("PRAGMA temp_store = MEMORY;") + cur.execute("PRAGMA mmap_size = 536870912;") + + # Drop table to recreate with new schema + cur.execute("DROP TABLE IF EXISTS cheatsheet;") + + # Create cheatsheet table with hash_id and WITHOUT ROWID + cur.execute( + """ + CREATE TABLE cheatsheet ( + hash_id INTEGER PRIMARY KEY, + category TEXT NOT NULL, + slug TEXT NOT NULL, + content TEXT NOT NULL, + title TEXT, + description TEXT, + keywords TEXT DEFAULT '[]' + ) WITHOUT ROWID; + """ + ) + cur.execute("CREATE INDEX IF NOT EXISTS idx_cheatsheet_category ON cheatsheet(category);") + cur.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_cheatsheet_category_slug ON cheatsheet(category, slug);") + + # Create category table + cur.execute( + """ + CREATE TABLE IF NOT EXISTS category ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + description TEXT, + keywords TEXT DEFAULT '[]', + features TEXT DEFAULT '[]' + ); + """ + ) + + # Create overview table + cur.execute( + """ + CREATE TABLE IF NOT EXISTS overview ( + id INTEGER PRIMARY KEY CHECK(id = 1), + total_count INTEGER NOT NULL + ); + """ + ) + + conn.commit() + +def extract_metadata(html_content: str): + metatags = {} + + # Extract title + title_match = re.search(r']*>([^<]*)', html_content, re.IGNORECASE) + if title_match: + metatags['title'] = title_match.group(1).strip() + + # Extract meta description + desc_match = re.search(r']*name=["\']description["\'][^>]*content=["\']([^"\']*)["\']', html_content, re.IGNORECASE) + if desc_match: + metatags['description'] = desc_match.group(1) + + # Extract keywords + keywords_match = re.search(r']*name=["\']keywords["\'][^>]*content=["\']([^"\']*)["\']', html_content, re.IGNORECASE) + if keywords_match: + metatags['keywords'] = [k.strip() for k in keywords_match.group(1).split(',')] + else: + metatags['keywords'] = [] + + # Extract body content + body_match = re.search(r']*>([\s\S]*?)', html_content, re.IGNORECASE) + if body_match: + metatags['content'] = body_match.group(1).strip() + else: + metatags['content'] = html_content + + return metatags + +def slugify(text: str) -> str: + # Match logic from frontend/src/lib/cheatsheets-utils.ts: + # .toLowerCase() + # .replace(/[^a-z0-9_]+/g, '-') + # .replace(/_/g, '_') <-- This seems redundant in JS if previous regex replaced _ with -, but let's follow the intent. + # Actually, looking at the JS code: .replace(/[^a-z0-9_]+/g, '-') keeps underscores. + # Then .replace(/_/g, '_') keeps underscores. + # Wait, the JS code is: + # .replace(/[^a-z0-9_]+/g, '-') -> replaces anything NOT a-z, 0-9, or _ with - + # .replace(/_/g, '_') -> replaces _ with _ (no-op?) + + # Let's replicate the exact behavior of the first replace, which is the meaningful one. + # It replaces any sequence of characters that are NOT lowercase letters, numbers, or underscores with a hyphen. + + return re.sub(r'[^a-zA-Z0-9_\-\.\+]+', '-', text) + +def process_cheatsheets(conn: sqlite3.Connection, limit: int = None): + cur = conn.cursor() + + if not DATA_DIR.exists(): + print(f"Data directory not found: {DATA_DIR}") + return + + html_files = list(DATA_DIR.glob("**/*.html")) + if limit: + html_files = html_files[:limit] + + print(f"Found {len(html_files)} cheatsheets to process...") + + categories = {} + inserted_count = 0 + + for file_path in html_files: + try: + # Path structure: data/cheatsheets//.html + category_name = file_path.parent.name + name = file_path.stem + category_slug = slugify(category_name) + cheatsheet_slug = slugify(name) + + content = file_path.read_text(encoding='utf-8') + metadata = extract_metadata(content) + + # Generate hash_id + hash_id = generate_hash_id(category_slug, cheatsheet_slug) + + # Insert into cheatsheet table + cur.execute( + """ + INSERT INTO cheatsheet (hash_id, category, slug, content, title, description, keywords) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + hash_id, + category_slug, + cheatsheet_slug, + metadata.get('content', ''), + metadata.get('title', ''), + metadata.get('description', ''), + json.dumps(metadata.get('keywords', [])) + ) + ) + + # Collect category info + if category_slug not in categories: + categories[category_slug] = { + 'name': category_name, + 'slug': category_slug, + 'description': '', # Empty as requested + 'keywords': [], # Empty as requested + 'features': [] # Empty as requested + } + + inserted_count += 1 + print(f"Processed: {category_name}/{name}") + + except Exception as e: + print(f"Error processing {file_path}: {e}") + + # Insert categories + for cat_slug, cat_data in categories.items(): + cur.execute( + """ + INSERT INTO category (name, slug, description, keywords, features) + VALUES (?, ?, ?, ?, ?) + """, + ( + cat_data['name'], + cat_data['slug'], + cat_data['description'], + json.dumps(cat_data['keywords']), + json.dumps(cat_data['features']) + ) + ) + + # Update overview + cur.execute("DELETE FROM overview") + cur.execute("INSERT INTO overview (id, total_count) VALUES (1, ?)", (inserted_count,)) + + conn.commit() + print(f"Successfully inserted {inserted_count} cheatsheets and {len(categories)} categories.") + +def main(): + # Ensure DB directory exists + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + + # Remove existing DB to start fresh + if DB_PATH.exists(): + DB_PATH.unlink() + + with sqlite3.connect(DB_PATH) as conn: + ensure_schema(conn) + process_cheatsheets(conn) + +if __name__ == "__main__": + main() diff --git a/frontend/scripts/convert-dbs-to-wal-mode.sh b/frontend/scripts/convert-dbs-to-wal-mode.sh new file mode 100755 index 0000000000..db31f5ca2b --- /dev/null +++ b/frontend/scripts/convert-dbs-to-wal-mode.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Convert emoji and PNG databases to WAL mode for better concurrent read performance +# Run this when the server is stopped + +EMOJI_DB="db/all_dbs/emoji-db-v1.db" +PNG_DB="db/all_dbs/png-icons-db-v1.db" + +convert_to_wal() { + local db_path=$1 + local db_name=$2 + + if [ ! -f "$db_path" ]; then + echo "⚠️ Warning: Database file not found: $db_path (skipping)" + return 1 + fi + + echo "Converting $db_name to WAL mode..." + + # Check current mode + current_mode=$(sqlite3 "$db_path" "PRAGMA journal_mode;" 2>/dev/null) + echo " Current mode: $current_mode" + + if [ "$current_mode" = "wal" ]; then + echo " ✅ Already in WAL mode" + return 0 + fi + + # Convert to WAL mode + sqlite3 "$db_path" "PRAGMA journal_mode=WAL; VACUUM;" 2>&1 + + if [ $? -eq 0 ]; then + new_mode=$(sqlite3 "$db_path" "PRAGMA journal_mode;" 2>/dev/null) + echo " ✅ Successfully converted to WAL mode (now: $new_mode)" + return 0 + else + echo " ❌ Failed to convert to WAL mode" + return 1 + fi +} + +echo "=== Converting databases to WAL mode ===" +echo "" + +convert_to_wal "$EMOJI_DB" "Emoji database" +echo "" + +convert_to_wal "$PNG_DB" "PNG database" +echo "" + +echo "=== Conversion complete ===" +echo "" +echo "After restarting the server, you should see -wal and -shm files for both databases." +echo "These files enable better concurrent read performance." + diff --git a/frontend/scripts/emojis/add-emoji-hash-columns.js b/frontend/scripts/emojis/add-emoji-hash-columns.js new file mode 100755 index 0000000000..91c025ef16 --- /dev/null +++ b/frontend/scripts/emojis/add-emoji-hash-columns.js @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +import Database from 'better-sqlite3'; +import crypto from 'crypto'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Get database path from command line argument or use default +const dbArg = process.argv[2]; +const config = { + dbPath: dbArg + ? path.resolve(process.cwd(), dbArg) + : path.join(__dirname, 'emoji-db-v1.db'), +}; + +function hashToKey(value) { + const hash = crypto + .createHash('sha256') + .update(value || '') + .digest(); + return hash.readBigInt64BE(0); +} + +function openDatabase() { + return new Database(config.dbPath, { fileMustExist: true }); +} + +function ensureHashColumns(db) { + console.log('🔄 Checking and adding hash columns...'); + + // Check if columns exist + const tableInfo = db.prepare('PRAGMA table_info(emojis)').all(); + const columnNames = tableInfo.map((col) => col.name); + + const hasSlugHash = columnNames.includes('slug_hash'); + const hasCategoryHash = columnNames.includes('category_hash'); + + if (!hasSlugHash) { + console.log(' Adding slug_hash column...'); + db.exec('ALTER TABLE emojis ADD COLUMN slug_hash INTEGER'); + } else { + console.log(' slug_hash column already exists'); + } + + if (!hasCategoryHash) { + console.log(' Adding category_hash column...'); + db.exec('ALTER TABLE emojis ADD COLUMN category_hash INTEGER'); + } else { + console.log(' category_hash column already exists'); + } + + return { hasSlugHash, hasCategoryHash }; +} + +function populateHashColumns(db) { + console.log('🔄 Populating hash columns...'); + + // Read all emoji data + const rows = db + .prepare( + ` + SELECT id, slug, category + FROM emojis + ` + ) + .all(); + + console.log(` Processing ${rows.length.toLocaleString()} emoji rows...`); + + // Generate hashes for all rows + const updates = rows.map((row) => ({ + id: row.id, + slug_hash: hashToKey(row.slug || ''), + category_hash: hashToKey(row.category || ''), + })); + + // Update rows with hashes + const updateStmt = db.prepare(` + UPDATE emojis + SET slug_hash = ?, category_hash = ? + WHERE id = ? + `); + + const updateBatch = db.transaction((entries) => { + for (const entry of entries) { + updateStmt.run(entry.slug_hash, entry.category_hash, entry.id); + } + }); + + updateBatch(updates); + + console.log( + `✅ Updated ${updates.length.toLocaleString()} rows with hash values` + ); +} + +function verifyHashes(db) { + console.log('🔄 Verifying hash columns...'); + + const rowsWithHashes = db + .prepare( + ` + SELECT COUNT(*) as count + FROM emojis + WHERE slug_hash IS NOT NULL AND category_hash IS NOT NULL + ` + ) + .get(); + + const totalRows = db.prepare('SELECT COUNT(*) as count FROM emojis').get(); + + console.log( + ` ${rowsWithHashes.count.toLocaleString()} / ${totalRows.count.toLocaleString()} rows have hash values` + ); + + if (rowsWithHashes.count === totalRows.count) { + console.log('✅ All rows have hash values'); + } else { + console.warn( + `⚠️ ${totalRows.count - rowsWithHashes.count} rows missing hash values` + ); + } +} + +async function main() { + const db = openDatabase(); + + console.log('🚀 Starting emoji hash columns migration...\n'); + console.log(`📁 Database: ${config.dbPath}\n`); + + try { + // Begin transaction + db.exec('BEGIN TRANSACTION'); + + // Step 1: Ensure columns exist + const { hasSlugHash, hasCategoryHash } = ensureHashColumns(db); + + // Step 2: Populate hash columns (only if they were just added or need updating) + if (!hasSlugHash || !hasCategoryHash) { + populateHashColumns(db); + } else { + // Check if columns are populated + const rowsWithHashes = db + .prepare( + ` + SELECT COUNT(*) as count + FROM emojis + WHERE slug_hash IS NOT NULL AND category_hash IS NOT NULL + ` + ) + .get(); + + if (rowsWithHashes.count === 0) { + console.log(' Columns exist but are empty, populating...'); + populateHashColumns(db); + } else { + console.log(' Hash columns already populated'); + } + } + + // Commit transaction + db.exec('COMMIT'); + + // Step 3: Verify + verifyHashes(db); + + console.log('\n✅ Migration complete!'); + } catch (error) { + console.error('❌ Error adding hash columns:', error); + db.exec('ROLLBACK'); + throw error; + } finally { + db.close(); + } +} + +main().catch((err) => { + console.error('❌ Error:', err); + process.exit(1); +}); diff --git a/frontend/scripts/emojis/migrate-emoji-db.js b/frontend/scripts/emojis/migrate-emoji-db.js new file mode 100755 index 0000000000..8dc3dcb44d --- /dev/null +++ b/frontend/scripts/emojis/migrate-emoji-db.js @@ -0,0 +1,308 @@ +#!/usr/bin/env node +/** + * Migration script for emoji database + * + * 1. Add emoji_slug_hash column to images table + * 2. Create images_new table with emoji_slug_hash as PRIMARY KEY (WITHOUT ROWID) + * 3. Create emojis_new table with slug_hash as PRIMARY KEY (WITHOUT ROWID) + * 4. Create category table with preview_emojis JSON column + */ + +import { Database } from 'bun:sqlite'; +import crypto from 'crypto'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { existsSync } from 'fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Hash function matching hash-utils.ts +function hashSlugToKey(slug) { + const hash = crypto.createHash('sha256').update(slug).digest(); + return hash.readBigInt64BE(0).toString(); +} + +// Hash emoji_slug + image_type for images table (composite key) +function hashImageKey(emojiSlug, imageType) { + const combined = `${emojiSlug}|${imageType}`; + const hash = crypto.createHash('sha256').update(combined).digest(); + return hash.readBigInt64BE(0).toString(); +} + +// Path to database +const dbPath = path.join(__dirname, 'emoji-db-v1.db'); + +console.log(`Opening database: ${dbPath}`); + +// Check if database exists +if (!existsSync(dbPath)) { + console.error(`❌ Error: Database file not found at ${dbPath}`); + process.exit(1); +} + +const db = new Database(dbPath); + +// Enable WAL mode for better performance +db.run('PRAGMA journal_mode = WAL;'); + +// Wrap operations in transaction for safety +let transactionStarted = false; +try { + db.run('BEGIN TRANSACTION;'); + transactionStarted = true; + + console.log('\n=== Step 1: Add emoji_slug_hash column to images table ==='); +try { + db.run('ALTER TABLE images ADD COLUMN emoji_slug_hash INTEGER;'); + console.log('✅ Added emoji_slug_hash column'); + + // Populate emoji_slug_hash for all rows (hash of emoji_slug + image_type for uniqueness) + console.log('Populating emoji_slug_hash values...'); + const images = db.prepare('SELECT emoji_slug, image_type FROM images').all(); + const updateStmt = db.prepare('UPDATE images SET emoji_slug_hash = ? WHERE emoji_slug = ? AND image_type = ?'); + + let count = 0; + for (const img of images) { + const hash = hashImageKey(img.emoji_slug, img.image_type); + updateStmt.run(hash, img.emoji_slug, img.image_type); + count++; + if (count % 1000 === 0) { + console.log(` Updated ${count} rows...`); + } + } + console.log(`✅ Populated emoji_slug_hash for ${count} rows`); +} catch (error) { + if (error.message.includes('duplicate column')) { + console.log('⚠️ emoji_slug_hash column already exists, skipping...'); + } else { + throw error; + } +} + +console.log('\n=== Step 2: Create images_new table ==='); +db.run(` + CREATE TABLE IF NOT EXISTS images_new ( + emoji_slug_hash INTEGER PRIMARY KEY, + emoji_slug TEXT NOT NULL, + filename TEXT NOT NULL, + image_data BLOB NOT NULL, + image_type TEXT NOT NULL + ) WITHOUT ROWID +`); +console.log('✅ Created images_new table (emoji_slug_hash = hash of emoji_slug + image_type)'); + +console.log('\n=== Step 3: Migrate data from images to images_new ==='); +console.log('Note: Multiple filenames per emoji_slug+image_type - selecting latest version...'); +// For each emoji_slug + image_type, pick the latest filename (highest iOS version or latest Discord version) +// Use a subquery to rank and select the best one +const migrateImages = db.prepare(` + INSERT INTO images_new (emoji_slug_hash, emoji_slug, filename, image_data, image_type) + SELECT + emoji_slug_hash, + emoji_slug, + filename, + image_data, + image_type + FROM ( + SELECT + emoji_slug_hash, + emoji_slug, + filename, + image_data, + image_type, + ROW_NUMBER() OVER ( + PARTITION BY emoji_slug_hash + ORDER BY + CASE + WHEN filename LIKE '%iOS%' THEN + CAST(SUBSTR(filename, INSTR(filename, 'iOS') + 4, 10) AS REAL) + WHEN image_type = 'twemoji-vendor' THEN + CAST(SUBSTR(filename, INSTR(filename, '_') + 1, 10) AS REAL) + ELSE 0 + END DESC, + filename DESC + ) as rn + FROM images + WHERE emoji_slug_hash IS NOT NULL + ) + WHERE rn = 1 +`); + +const result = migrateImages.run(); +console.log(`✅ Migrated ${result.changes} rows to images_new`); + +console.log('\n=== Step 4: Drop old images table ==='); +db.run('DROP TABLE images'); +console.log('✅ Dropped images table'); + +console.log('\n=== Step 5: Rename images_new to images ==='); +db.run('ALTER TABLE images_new RENAME TO images'); +console.log('✅ Renamed images_new to images'); + +console.log('\n=== Step 6: Create emojis_new table ==='); +db.run(` + CREATE TABLE IF NOT EXISTS emojis_new ( + slug_hash INTEGER PRIMARY KEY, + id INTEGER NOT NULL, + code TEXT NOT NULL, + unicode TEXT NOT NULL, + slug TEXT NOT NULL, + title TEXT NOT NULL, + category TEXT, + description TEXT, + apple_vendor_description TEXT, + keywords TEXT, + also_known_as TEXT, + version TEXT, + senses TEXT, + shortcodes TEXT, + discord_vendor_description TEXT, + category_hash INTEGER + ) WITHOUT ROWID +`); +console.log('✅ Created emojis_new table'); + +console.log('\n=== Step 7: Migrate data from emojis to emojis_new ==='); +const migrateEmojis = db.prepare(` + INSERT INTO emojis_new ( + slug_hash, id, code, unicode, slug, title, category, + description, apple_vendor_description, keywords, also_known_as, + version, senses, shortcodes, discord_vendor_description, category_hash + ) + SELECT + slug_hash, id, code, unicode, slug, title, category, + description, apple_vendor_description, keywords, also_known_as, + version, senses, shortcodes, discord_vendor_description, category_hash + FROM emojis + WHERE slug_hash IS NOT NULL +`); + +const emojiResult = migrateEmojis.run(); +console.log(`✅ Migrated ${emojiResult.changes} rows to emojis_new`); + +console.log('\n=== Step 8: Drop old emojis table ==='); +db.run('DROP TABLE emojis'); +console.log('✅ Dropped emojis table'); + +console.log('\n=== Step 9: Rename emojis_new to emojis ==='); +db.run('ALTER TABLE emojis_new RENAME TO emojis'); +console.log('✅ Renamed emojis_new to emojis'); + +console.log('\n=== Step 10: Create category table ==='); +db.run(` + CREATE TABLE IF NOT EXISTS category ( + category_hash INTEGER PRIMARY KEY, + category TEXT NOT NULL, + count INTEGER NOT NULL, + preview_emojis_json TEXT NOT NULL DEFAULT '[]' + ) WITHOUT ROWID +`); +console.log('✅ Created category table'); + +console.log('\n=== Step 11: Populate category table ==='); +// Get all categories with counts +const categories = db.prepare(` + SELECT + category_hash, + category, + COUNT(*) as count + FROM emojis + WHERE category IS NOT NULL AND category_hash IS NOT NULL + GROUP BY category_hash, category +`).all(); + +console.log(`Found ${categories.length} categories`); + +// For each category, get first 5 emojis for preview (all columns) +// Use category name instead of category_hash to ensure we get the right emojis +const getPreviewEmojis = db.prepare(` + SELECT + id, code, unicode, slug, title, category, + description, apple_vendor_description, keywords, also_known_as, + version, senses, shortcodes, discord_vendor_description, + slug_hash, category_hash + FROM emojis + WHERE category = ? + ORDER BY + CASE WHEN slug LIKE '%-skin-tone%' OR slug LIKE '%skin-tone%' THEN 1 ELSE 0 END, + COALESCE(title, slug) COLLATE NOCASE + LIMIT 5 +`); + +const insertCategory = db.prepare(` + INSERT INTO category (category_hash, category, count, preview_emojis_json) + VALUES (?, ?, ?, ?) +`); + +let categoryCount = 0; +for (const cat of categories) { + const previewEmojis = getPreviewEmojis.all(cat.category); + // Include all columns in the preview JSON + const previewJson = JSON.stringify(previewEmojis); + + insertCategory.run( + cat.category_hash, + cat.category, + cat.count, + previewJson + ); + + categoryCount++; + if (categoryCount % 10 === 0) { + console.log(` Processed ${categoryCount} categories...`); + } +} + +console.log(`✅ Populated category table with ${categoryCount} categories`); + +console.log('\n=== Step 12: Create indexes ==='); +// Create index on emojis.category_hash for faster category lookups +db.run('CREATE INDEX IF NOT EXISTS idx_emojis_category_hash ON emojis(category_hash)'); +console.log('✅ Created index on emojis.category_hash'); + +// Create index on images.emoji_slug for backward compatibility (if needed) +db.run('CREATE INDEX IF NOT EXISTS idx_images_emoji_slug ON images(emoji_slug)'); +console.log('✅ Created index on images.emoji_slug'); + +console.log('\n=== Migration Complete! ==='); +console.log('\nSummary:'); +console.log(' ✅ Added emoji_slug_hash to images table'); +console.log(' ✅ Migrated images table to WITHOUT ROWID with emoji_slug_hash as PK'); +console.log(' ✅ Migrated emojis table to WITHOUT ROWID with slug_hash as PK'); +console.log(' ✅ Created category table with preview_emojis_json'); +console.log(' ✅ Created indexes for performance'); + +// Verify counts +const emojiCount = db.prepare('SELECT COUNT(*) as count FROM emojis').get(); +const imageCount = db.prepare('SELECT COUNT(*) as count FROM images').get(); +const categoryCountFinal = db.prepare('SELECT COUNT(*) as count FROM category').get(); + +console.log('\nFinal counts:'); +console.log(` Emojis: ${emojiCount.count}`); +console.log(` Images: ${imageCount.count}`); +console.log(` Categories: ${categoryCountFinal.count}`); + + // Commit transaction + db.run('COMMIT;'); + console.log('\n✅ Transaction committed'); + + db.close(); + console.log('\n✅ Database migration completed successfully!'); + +} catch (error) { + console.error('\n❌ Error during migration:', error.message); + console.error(error.stack); + if (transactionStarted) { + console.log('Rolling back transaction...'); + try { + db.run('ROLLBACK;'); + console.log('✅ Transaction rolled back'); + } catch (rollbackError) { + console.error('❌ Failed to rollback:', rollbackError.message); + } + } + db.close(); + process.exit(1); +} + diff --git a/frontend/scripts/man-pages/migrate_manpages_db.py b/frontend/scripts/man-pages/migrate_manpages_db.py new file mode 100644 index 0000000000..3833f76432 --- /dev/null +++ b/frontend/scripts/man-pages/migrate_manpages_db.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Migrate Man-Pages DB to use Hash-based Primary Key. + +Source: db/all_dbs/man-pages-db.db +Destination: db/all_dbs/man-pages-new-db.db + +Changes: +1. Create new schema with hash_id for man_pages and sub_category. +2. man_pages table uses main_category_hash and sub_category_hash (integers) instead of text. +3. Migrate data from old DB to new DB. +4. Add 'sub_category_count' to 'category' table and populate it. +5. Add 'category_hash_id' to 'sub_category' and 'hash_id' to 'category' tables. +6. Populate hash IDs. +""" + +import sqlite3 +import hashlib +from pathlib import Path + +BASE_DIR = Path(__file__).parent.parent.parent +OLD_DB_PATH = BASE_DIR / "db" / "all_dbs" / "man-pages-db_old.db" +NEW_DB_PATH = BASE_DIR / "db" / "all_dbs" / "man-pages-db.db" + +def generate_hash_id(main_category: str, sub_category: str, slug: str) -> int: + """ + Generate a 64-bit signed integer hash from category, sub_category, and slug. + """ + combined = f"{main_category}{sub_category}{slug}" + hash_bytes = hashlib.sha256(combined.encode('utf-8')).digest() + return int.from_bytes(hash_bytes[:8], byteorder='big', signed=True) + +def generate_subcategory_pk_hash(main_category: str, sub_category: str) -> int: + """ + Generate a 64-bit signed integer hash from main_category and sub_category. + Used for sub_category table Primary Key. + """ + combined = f"{main_category}{sub_category}" + hash_bytes = hashlib.sha256(combined.encode('utf-8')).digest() + return int.from_bytes(hash_bytes[:8], byteorder='big', signed=True) + +def generate_simple_hash(text: str) -> int: + """ + Generate a 64-bit signed integer hash from a single string. + Used for main_category_hash and sub_category_hash columns. + """ + hash_bytes = hashlib.sha256(text.encode('utf-8')).digest() + return int.from_bytes(hash_bytes[:8], byteorder='big', signed=True) + +def migrate_db(): + if not OLD_DB_PATH.exists(): + print(f"Source DB not found: {OLD_DB_PATH}") + return + + # Remove existing new DB if it exists + if NEW_DB_PATH.exists(): + NEW_DB_PATH.unlink() + + print(f"Migrating from {OLD_DB_PATH.name} to {NEW_DB_PATH.name}...") + + with sqlite3.connect(OLD_DB_PATH) as old_conn, sqlite3.connect(NEW_DB_PATH) as new_conn: + old_cur = old_conn.cursor() + new_cur = new_conn.cursor() + + # --- 1. Setup New Schema --- + # Performance PRAGMAs + new_cur.execute("PRAGMA journal_mode = WAL;") + new_cur.execute("PRAGMA synchronous = OFF;") + new_cur.execute("PRAGMA cache_size = -128000;") + new_cur.execute("PRAGMA temp_store = MEMORY;") + new_cur.execute("PRAGMA mmap_size = 536870912;") + + # Create 'man_pages' table with hash_id and hashed categories + new_cur.execute( + """ + CREATE TABLE man_pages ( + hash_id INTEGER PRIMARY KEY, + category_hash INTEGER NOT NULL, + main_category TEXT NOT NULL, + sub_category TEXT NOT NULL, + title TEXT NOT NULL, + slug TEXT NOT NULL, + filename TEXT NOT NULL, + content TEXT DEFAULT '{}' + ) WITHOUT ROWID; + """ + ) + + # Create 'category' table + new_cur.execute( + """ + CREATE TABLE category ( + name TEXT, + count INTEGER NOT NULL DEFAULT 0, + description TEXT DEFAULT '', + keywords TEXT DEFAULT '[]', + path TEXT DEFAULT '', + sub_category_count INTEGER DEFAULT 0, + hash_id INTEGER PRIMARY KEY + ) WITHOUT ROWID; + """ + ) + + # Create 'sub_category' table + new_cur.execute( + """ + CREATE TABLE sub_category ( + hash_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + count INTEGER NOT NULL DEFAULT 0, + description TEXT DEFAULT '', + keywords TEXT DEFAULT '[]', + path TEXT DEFAULT '', + main_category_hash INTEGER + ) WITHOUT ROWID; + """ + ) + + # Create 'overview' table + new_cur.execute( + """ + CREATE TABLE overview ( + id INTEGER PRIMARY KEY CHECK(id = 1), + total_count INTEGER NOT NULL DEFAULT 0 + ); + """ + ) + + + # --- 2. Migrate Data --- + + # Migrate 'man_pages' + print("Migrating man_pages...") + old_cur.execute("SELECT main_category, sub_category, title, slug, filename, content FROM man_pages") + rows = old_cur.fetchall() + + inserted_count = 0 + for row in rows: + main_cat, sub_cat, title, slug, filename, content = row + hash_id = generate_hash_id(main_cat, sub_cat, slug) + # category_hash in man_pages corresponds to hash_id in sub_category (main + sub) + category_hash = generate_subcategory_pk_hash(main_cat, sub_cat) + + try: + new_cur.execute( + """ + INSERT INTO man_pages (hash_id, category_hash, main_category, sub_category, title, slug, filename, content) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + (hash_id, category_hash, main_cat, sub_cat, title, slug, filename, content) + ) + inserted_count += 1 + except sqlite3.IntegrityError as e: + print(f"Skipping duplicate or error for {main_cat}/{sub_cat}/{slug}: {e}") + + print(f"Migrated {inserted_count} man_pages.") + + # Migrate 'category' + print("Migrating category...") + old_cur.execute("SELECT name, count, description, keywords, path FROM category") + categories = old_cur.fetchall() + + print("Calculating sub_category counts...") + old_cur.execute(""" + SELECT main_category, COUNT(DISTINCT sub_category) + FROM man_pages + GROUP BY main_category + """) + cat_counts = dict(old_cur.fetchall()) + + cat_inserted = 0 + for row in categories: + name, count, description, keywords, path = row + sub_cat_count = cat_counts.get(name, 0) + hash_id = generate_simple_hash(name) + + new_cur.execute( + """ + INSERT INTO category (name, count, description, keywords, path, sub_category_count, hash_id) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + (name, count, description, keywords, path, sub_cat_count, hash_id) + ) + cat_inserted += 1 + + print(f"Migrated {cat_inserted} categories.") + + # Migrate 'sub_category' + print("Migrating sub_category...") + old_cur.execute(""" + SELECT + m.main_category, + m.sub_category, + COUNT(m.slug) as calculated_count, + s.description, + s.keywords, + s.path + FROM man_pages m + LEFT JOIN sub_category s ON m.sub_category = s.name + GROUP BY m.main_category, m.sub_category + """) + sub_categories = old_cur.fetchall() + + sub_cat_inserted = 0 + for row in sub_categories: + main_cat, sub_cat_name, count, description, keywords, path = row + count = count or 0 + description = description or '' + keywords = keywords or '[]' + path = path or '' + + hash_id = generate_subcategory_pk_hash(main_cat, sub_cat_name) + category_hash_id = generate_simple_hash(main_cat) + + try: + new_cur.execute( + """ + INSERT INTO sub_category (hash_id, name, count, description, keywords, path, main_category_hash) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + (hash_id, sub_cat_name, count, description, keywords, path, category_hash_id) + ) + sub_cat_inserted += 1 + except sqlite3.IntegrityError as e: + print(f"Skipping duplicate sub_category {main_cat}/{sub_cat_name}: {e}") + + print(f"Migrated {sub_cat_inserted} sub_categories.") + + # Migrate 'overview' + print("Migrating overview...") + old_cur.execute("SELECT * FROM overview") + overview = old_cur.fetchall() + new_cur.executemany("INSERT INTO overview VALUES (?, ?)", overview) + + new_conn.commit() + print("Migration complete.") + +if __name__ == "__main__": + migrate_db() diff --git a/frontend/scripts/mcp/build_mcp_db.py b/frontend/scripts/mcp/build_mcp_db.py new file mode 100644 index 0000000000..93308efe2e --- /dev/null +++ b/frontend/scripts/mcp/build_mcp_db.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +""" +Build MCP Database from JSON input files. + +Source: frontend/public/mcp/input/*.json +Destination: db/all_dbs/mcp-db.db + +Tables: +- overview: id, total_count +- category: slug, name, description, count +- mcp_pages: hash_id, category_slug, mcp_key, name, description, owner, stars, forks, language, license, updated_at, readme_content, url, image_url, npm_url, npm_downloads, keywords +""" + +import sqlite3 +import json +import hashlib +import glob +import re +from pathlib import Path +import markdown +from bs4 import BeautifulSoup + +BASE_DIR = Path(__file__).parent.parent.parent +INPUT_DIR = BASE_DIR / "public" / "mcp" / "input" +DB_PATH = BASE_DIR / "db" / "all_dbs" / "mcp-db.db" + +def generate_category_hash(category_slug: str) -> int: + """ + Generate a 64-bit signed integer hash from category_slug. + """ + hash_bytes = hashlib.sha256(category_slug.encode('utf-8')).digest() + # Take first 8 bytes, interpret as big-endian signed integer + return int.from_bytes(hash_bytes[:8], byteorder='big', signed=True) + +def generate_hash_id(category_slug: str, mcp_key: str) -> int: + """ + Generate a 64-bit signed integer hash from category_slug and mcp_key. + """ + combined = f"{category_slug}{mcp_key}" + hash_bytes = hashlib.sha256(combined.encode('utf-8')).digest() + # Take first 8 bytes, interpret as big-endian signed integer + return int.from_bytes(hash_bytes[:8], byteorder='big', signed=True) + +def process_readme_content(markdown_text: str) -> str: + """ + Convert Markdown to HTML and apply transformations: + - Add IDs to headings + - Add scroll margin to headings + - Open external links in new tab + - Disable relative links + """ + if not markdown_text: + return '' + + try: + # Convert Markdown to HTML + html = markdown.markdown(markdown_text, extensions=['fenced_code', 'tables']) + soup = BeautifulSoup(html, 'html.parser') + + # Process Headings + for tag in soup.find_all(re.compile('^h[1-6]$')): + # Generate ID + text = tag.get_text() + anchor_id = re.sub(r'[^a-z0-9\s-]', '', text.lower()) + anchor_id = re.sub(r'\s+', '-', anchor_id) + anchor_id = re.sub(r'-+', '-', anchor_id).strip('-') + + tag['id'] = anchor_id + + # Add scroll margin class and style + existing_class = tag.get('class', []) + if 'scroll-mt-32' not in existing_class: + existing_class.append('scroll-mt-32') + tag['class'] = existing_class + + # Add inline style + existing_style = tag.get('style', '') + tag['style'] = f"{existing_style}; scroll-margin-top: 8rem;".strip('; ') + + # Process Links + for tag in soup.find_all('a'): + href = tag.get('href', '') + + if href.startswith(('http://', 'https://')): + tag['target'] = '_blank' + tag['rel'] = 'noopener noreferrer' + elif href.startswith('#'): + # Keep anchor links + pass + else: + # Disable relative links by changing tag to span + tag.name = 'span' + # Remove href attribute + del tag['href'] + # Add disabled styling if needed (optional, logic in frontend was just span) + + return str(soup) + except Exception as e: + print(f"Error processing README: {e}") + return markdown_text + +def build_db(): + # Ensure DB directory exists + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + + # Remove existing DB if it exists + if DB_PATH.exists(): + DB_PATH.unlink() + + print(f"Building MCP DB at {DB_PATH}...") + + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + +import multiprocessing +from functools import partial + +def process_json_file(json_file): + """ + Process a single JSON file and return data for insertion. + Returns a tuple: (category_data, mcp_pages_data, error_msg) + """ + try: + with open(json_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + category_slug = data.get('category', '') + category_name = data.get('categoryDisplay', '') + category_desc = data.get('description', '') + repositories = data.get('repositories', {}) + + if not category_slug: + return None, None, f"Skipping {Path(json_file).name}: No category slug found." + + repo_count = len(repositories) + category_hash_id = generate_category_hash(category_slug) + + category_data = (category_slug, category_name, category_desc, repo_count) + + mcp_pages_data = [] + for mcp_key, repo_data in repositories.items(): + hash_id = generate_hash_id(category_slug, mcp_key) + + name = repo_data.get('name', '') + description = repo_data.get('description', '') + owner = repo_data.get('owner', '') + stars = repo_data.get('stars', 0) + forks = repo_data.get('forks', 0) + language = repo_data.get('language', '') + license_name = repo_data.get('license', '') + updated_at = repo_data.get('updated_at', '') + + raw_readme = repo_data.get('readme_content', '') + readme_content = process_readme_content(raw_readme) + + url = repo_data.get('url', '') + image_url = repo_data.get('imageUrl', '') + npm_url = repo_data.get('npm_url', '') + npm_downloads = repo_data.get('npm_downloads', 0) + keywords = json.dumps(repo_data.get('keywords', [])) + + mcp_pages_data.append(( + hash_id, category_hash_id, mcp_key, name, description, + owner, stars, forks, language, license_name, updated_at, + readme_content, url, image_url, npm_url, npm_downloads, keywords + )) + + return category_data, mcp_pages_data, None + + except Exception as e: + return None, None, f"Error processing {json_file}: {e}" + +def build_db(): + # Ensure DB directory exists + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + + # Remove existing DB if it exists + if DB_PATH.exists(): + DB_PATH.unlink() + + print(f"Building MCP DB at {DB_PATH}...") + + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + # 1. Create Tables + + # Overview Table + cur.execute(""" + CREATE TABLE overview ( + id INTEGER PRIMARY KEY CHECK(id = 1), + total_count INTEGER NOT NULL DEFAULT 0, + total_category_count INTEGER NOT NULL DEFAULT 0 + ); + """) + + # Category Table + cur.execute(""" + CREATE TABLE category ( + slug TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT DEFAULT '', + count INTEGER NOT NULL DEFAULT 0 + ); + """) + + # MCP Pages Table + cur.execute(""" + CREATE TABLE mcp_pages ( + hash_id INTEGER PRIMARY KEY, + category_id INTEGER NOT NULL, + key TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT DEFAULT '', + owner TEXT DEFAULT '', + stars INTEGER DEFAULT 0, + forks INTEGER DEFAULT 0, + language TEXT DEFAULT '', + license TEXT DEFAULT '', + updated_at TEXT DEFAULT '', + readme_content TEXT DEFAULT '', + url TEXT DEFAULT '', + image_url TEXT DEFAULT '', + npm_url TEXT DEFAULT '', + npm_downloads INTEGER DEFAULT 0, + keywords TEXT DEFAULT '' + ) WITHOUT ROWID; + """) + + # Create Indexes + cur.execute("CREATE INDEX idx_mcp_pages_category_id ON mcp_pages(category_id);") + cur.execute("CREATE INDEX idx_mcp_category_stars_name ON mcp_pages(category_id, stars DESC, name ASC);") + + # 2. Process JSON Files + + json_files = glob.glob(str(INPUT_DIR / "*.json")) + total_mcp_count = 0 + + print(f"Processing {len(json_files)} files using {multiprocessing.cpu_count()} cores...") + + with multiprocessing.Pool() as pool: + results = pool.map(process_json_file, json_files) + + # 3. Insert Data + print("Inserting data into database...") + + for category_data, mcp_pages_data, error in results: + if error: + print(error) + continue + + if not category_data: + continue + + # Insert Category + try: + cur.execute( + "INSERT INTO category (slug, name, description, count) VALUES (?, ?, ?, ?)", + category_data + ) + except sqlite3.IntegrityError: + print(f"Category {category_data[0]} already exists, updating count...") + cur.execute( + "UPDATE category SET count = count + ? WHERE slug = ?", + (category_data[3], category_data[0]) + ) + + # Insert MCP Pages + if mcp_pages_data: + cur.executemany( + """ + INSERT INTO mcp_pages ( + hash_id, category_id, key, name, description, + owner, stars, forks, language, license, updated_at, + readme_content, url, image_url, npm_url, npm_downloads, keywords + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + mcp_pages_data + ) + total_mcp_count += len(mcp_pages_data) + + # 4. Update Overview + # Count total categories + cur.execute("SELECT COUNT(*) FROM category") + total_category_count = cur.fetchone()[0] + + cur.execute("INSERT INTO overview (id, total_count, total_category_count) VALUES (1, ?, ?)", (total_mcp_count, total_category_count)) + + conn.commit() + conn.close() + print(f"Successfully built MCP DB with {total_mcp_count} pages.") + +if __name__ == "__main__": + build_db() diff --git a/frontend/scripts/mcp/update_overview_schema.py b/frontend/scripts/mcp/update_overview_schema.py new file mode 100644 index 0000000000..5c60dc11c1 --- /dev/null +++ b/frontend/scripts/mcp/update_overview_schema.py @@ -0,0 +1,42 @@ +import sqlite3 +from pathlib import Path + +BASE_DIR = Path(__file__).parent.parent.parent +DB_PATH = BASE_DIR / "db" / "all_dbs" / "mcp-db.db" + +def update_overview_schema(): + print(f"Updating DB at {DB_PATH}...") + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + try: + # 1. Add column if it doesn't exist + try: + cur.execute("ALTER TABLE overview ADD COLUMN total_category_count INTEGER NOT NULL DEFAULT 0") + print("Added total_category_count column.") + except sqlite3.OperationalError as e: + if "duplicate column name" in str(e): + print("Column total_category_count already exists.") + else: + raise e + + # 2. Calculate total categories + cur.execute("SELECT COUNT(*) FROM category") + total_categories = cur.fetchone()[0] + print(f"Total categories found: {total_categories}") + + # 3. Update overview table + # Assuming row with id=1 exists as per build script + cur.execute("UPDATE overview SET total_category_count = ? WHERE id = 1", (total_categories,)) + print("Updated overview table.") + + conn.commit() + print("Success.") + + except Exception as e: + print(f"Error: {e}") + finally: + conn.close() + +if __name__ == "__main__": + update_overview_schema() diff --git a/frontend/scripts/png_icons_to_base64/add_preview_icons_column.ts b/frontend/scripts/png_icons_to_base64/add_preview_icons_column.ts new file mode 100644 index 0000000000..fd23bb2d59 --- /dev/null +++ b/frontend/scripts/png_icons_to_base64/add_preview_icons_column.ts @@ -0,0 +1,248 @@ +#!/usr/bin/env node +/** + * Add preview_icons_json column to PNG cluster table + * + * This script: + * 1. Reads cluster and icon data from existing tables + * 2. Precomputes preview icons for each cluster + * 3. Adds preview_icons_json column to cluster table if it doesn't exist + * 4. Populates the column with precomputed JSON data + * + * Run this after icon ingestion to add preview icons to clusters. + */ + +import Database from 'better-sqlite3'; +import path from 'path'; + +type DatabaseInstance = InstanceType; + +const DB_PATH = path.resolve(process.cwd(), 'db/bench/png/png-icons-db-v1.db'); + +interface PreviewIcon { + id: number; + name: string; + base64: string; + img_alt: string; +} + +interface ClusterRow { + id: number; + name: string; + source_folder: string; + path: string; + count: number; + keywords: string; + tags: string; + title: string; + description: string; + practical_application: string; + alternative_terms: string; + about: string; + why_choose_us: string; +} + +interface IconRow { + id: number; + cluster: string; + name: string; + base64: string; + img_alt: string; +} + +// Open database connection +function openDb(): DatabaseInstance { + return new Database(DB_PATH); +} + +// Step 1: Check if column exists, add if not +function ensurePreviewIconsColumn(db: DatabaseInstance): void { + console.log('[ADD_PREVIEW_ICONS] Checking if preview_icons_json column exists...'); + + // Check if column exists + const tableInfo = db.prepare("PRAGMA table_info(cluster)").all() as Array<{ name: string }>; + + const hasColumn = tableInfo.some(col => col.name === 'preview_icons_json'); + + if (!hasColumn) { + console.log('[ADD_PREVIEW_ICONS] Adding preview_icons_json column...'); + db.exec('ALTER TABLE cluster ADD COLUMN preview_icons_json TEXT DEFAULT \'[]\''); + console.log('[ADD_PREVIEW_ICONS] Column added successfully'); + } else { + console.log('[ADD_PREVIEW_ICONS] Column already exists'); + } +} + +// Step 2: Read cluster and icon data into memory +function readAllData(db: DatabaseInstance): { + clusters: ClusterRow[]; + icons: IconRow[]; +} { + console.log('[ADD_PREVIEW_ICONS] Reading cluster and icon data from existing tables...'); + const startTime = Date.now(); + + // Read clusters + const clusters = db.prepare(` + SELECT id, name, source_folder, path, count, keywords, tags, + title, description, practical_application, alternative_terms, + about, why_choose_us + FROM cluster ORDER BY name + `).all() as ClusterRow[]; + + // Read icons (needed for precomputing preview icons) + const icons = db.prepare(` + SELECT id, cluster, name, base64, img_alt + FROM icon ORDER BY name + `).all() as IconRow[]; + + const elapsed = Date.now() - startTime; + console.log(`[ADD_PREVIEW_ICONS] Read ${clusters.length} clusters, ${icons.length} icons in ${elapsed}ms`); + + return { + clusters, + icons, + }; +} + +// Step 3: Precompute preview icons for each cluster +function precomputePreviewIcons( + clusters: ClusterRow[], + icons: IconRow[], + previewIconsPerCluster: number = 6 +): Map { + console.log(`[ADD_PREVIEW_ICONS] Precomputing preview icons (${previewIconsPerCluster} per cluster)...`); + const startTime = Date.now(); + + // Group icons by cluster + const iconsByCluster = new Map(); + for (const icon of icons) { + if (!iconsByCluster.has(icon.cluster)) { + iconsByCluster.set(icon.cluster, []); + } + iconsByCluster.get(icon.cluster)!.push(icon); + } + + // Sort icons by name within each cluster + for (const [cluster, clusterIcons] of iconsByCluster.entries()) { + clusterIcons.sort((a, b) => a.name.localeCompare(b.name)); + } + + // Build preview icons for each cluster + const previewIconsMap = new Map(); + + for (const cluster of clusters) { + // Try both source_folder and name as cluster keys + const clusterKey = cluster.source_folder || cluster.name; + const clusterIcons = iconsByCluster.get(clusterKey) || []; + + const previewIcons: PreviewIcon[] = clusterIcons + .slice(0, previewIconsPerCluster) + .map((icon) => ({ + id: icon.id, + name: icon.name, + base64: icon.base64, + img_alt: icon.img_alt, + })); + + previewIconsMap.set(cluster.name, previewIcons); + } + + const elapsed = Date.now() - startTime; + console.log(`[ADD_PREVIEW_ICONS] Precomputed preview icons in ${elapsed}ms`); + + return previewIconsMap; +} + +// Step 4: Update cluster table with preview icons +function updatePreviewIcons( + db: DatabaseInstance, + clusters: ClusterRow[], + previewIconsMap: Map +): void { + console.log('[ADD_PREVIEW_ICONS] Updating cluster table with preview icons...'); + const startTime = Date.now(); + + const updateStmt = db.prepare('UPDATE cluster SET preview_icons_json = ? WHERE name = ?'); + + const updateBatch = db.transaction((entries: Array<{ json: string; name: string }>) => { + for (const entry of entries) { + updateStmt.run(entry.json, entry.name); + } + }); + + const updates = clusters.map(cluster => ({ + json: JSON.stringify(previewIconsMap.get(cluster.name) || []), + name: cluster.name, + })); + + updateBatch(updates); + + const elapsed = Date.now() - startTime; + console.log(`[ADD_PREVIEW_ICONS] Updated ${clusters.length} clusters in ${elapsed}ms`); +} + +// Step 5: Verify the updates +function verifyUpdates(db: DatabaseInstance): void { + console.log('[ADD_PREVIEW_ICONS] Verifying updates...'); + + const clusterCount = db.prepare( + 'SELECT COUNT(*) as count FROM cluster WHERE preview_icons_json IS NOT NULL AND preview_icons_json != \'[]\'' + ).get() as { count: number } | undefined; + console.log(`[ADD_PREVIEW_ICONS] ${clusterCount?.count || 0} clusters have preview icons`); + + // Sample preview icons + const sampleCluster = db.prepare( + 'SELECT name, preview_icons_json FROM cluster WHERE preview_icons_json IS NOT NULL AND preview_icons_json != \'[]\' LIMIT 1' + ).get() as { name: string; preview_icons_json: string } | undefined; + if (sampleCluster) { + const previewIcons = JSON.parse(sampleCluster.preview_icons_json || '[]'); + console.log(`[ADD_PREVIEW_ICONS] Sample cluster "${sampleCluster.name}" has ${previewIcons.length} preview icons`); + } +} + +// Main function +function main(): void { + console.log(`[ADD_PREVIEW_ICONS] Starting to add preview_icons_json column...`); + console.log(`[ADD_PREVIEW_ICONS] Database: ${DB_PATH}`); + + const db = openDb(); + + try { + // Begin transaction for better performance + db.exec('BEGIN TRANSACTION'); + + // Step 1: Ensure column exists + ensurePreviewIconsColumn(db); + + // Step 2: Read cluster and icon data + const { clusters, icons } = readAllData(db); + + // Step 3: Precompute preview icons + const previewIconsMap = precomputePreviewIcons(clusters, icons, 6); + + // Step 4: Update cluster table + updatePreviewIcons(db, clusters, previewIconsMap); + + // Commit transaction + db.exec('COMMIT'); + + // Step 5: Verify + verifyUpdates(db); + + console.log('[ADD_PREVIEW_ICONS] ✅ Successfully added preview_icons_json column and populated data!'); + } catch (error) { + console.error('[ADD_PREVIEW_ICONS] ❌ Error adding preview icons column:', error); + db.exec('ROLLBACK'); + throw error; + } finally { + db.close(); + } +} + +// Run the script +try { + main(); +} catch (error) { + console.error(error); + process.exit(1); +} + diff --git a/frontend/scripts/png_icons_to_base64/build_sqlite_from_json.py b/frontend/scripts/png_icons_to_base64/build_sqlite_from_json.py index b63d71853a..b506dfb053 100644 --- a/frontend/scripts/png_icons_to_base64/build_sqlite_from_json.py +++ b/frontend/scripts/png_icons_to_base64/build_sqlite_from_json.py @@ -4,7 +4,7 @@ - Scans scripts/svg_icons_to_base64/base64_svg_icons/*.json - Reads data/cluster_svg.json for metadata -- Creates SQLite DB at db/all_dbs/svg-icons-db.db +- Creates SQLite DB at db/all_dbs/svg-icons-db-v1.db - Table: icon(id INTEGER PRIMARY KEY AUTOINCREMENT, cluster TEXT, name TEXT, base64 TEXT, ...) - Table: cluster(name TEXT PRIMARY KEY, count INTEGER, source_folder TEXT, ...) - Table: overview(id INTEGER PRIMARY KEY CHECK(id = 1), total_count INTEGER) @@ -19,7 +19,7 @@ BASE_DIR = Path(__file__).parent JSON_DIR = BASE_DIR / "base64_svg_icons" CLUSTER_PNG_PATH = BASE_DIR.parent.parent / "data" / "cluster_png.json" -DB_PATH = Path(__file__).parent.parent.parent / "db" / "all_dbs" / "png-icons-db.db" +DB_PATH = Path(__file__).parent.parent.parent / "db" / "all_dbs" / "png-icons-db-v1.db" def ensure_schema(conn: sqlite3.Connection) -> None: diff --git a/frontend/scripts/sitemap-checker-v2/Makefile b/frontend/scripts/sitemap-checker-v2/Makefile new file mode 100644 index 0000000000..7c16924d66 --- /dev/null +++ b/frontend/scripts/sitemap-checker-v2/Makefile @@ -0,0 +1,67 @@ +# Sitemap Checker V2 - Extract all URLs from sitemaps + +SITEMAP_TYPE ?= svg + +# Sitemap URLs +PROD_SITEMAP_URL = https://hexmos.com/freedevtools/$(SITEMAP_TYPE)_icons/sitemap.xml +LOCAL_SITEMAP_URL = http://127.0.0.1/freedevtools/$(SITEMAP_TYPE)_icons/sitemap.xml +LOCAL_HOST_HEADER = hexmos-local.com + +# Binary name +BINARY = extract-urls + +# Timestamp for log folder +TIMESTAMP = $(shell date +%Y-%m-%d_%H-%M-%S) +LOG_DIR = logs/$(TIMESTAMP) + +.PHONY: build svg png emoji clean + +# Build the Go binary +build: + @go build -o $(BINARY) extract-urls.go + +# Create log directory +create-log-dir: + @mkdir -p $(LOG_DIR) + +# Extract SVG icons URLs +svg: build create-log-dir + @echo "Extracting SVG icons URLs..." + @PROD_OUTPUT="$(LOG_DIR)/urls-prod-svg.txt" && \ + LOCAL_OUTPUT="$(LOG_DIR)/urls-local-svg.txt" && \ + ./$(BINARY) -url "$(PROD_SITEMAP_URL)" -output "$$PROD_OUTPUT" && \ + ./$(BINARY) -url "$(LOCAL_SITEMAP_URL)" -output "$$LOCAL_OUTPUT" -host "$(LOCAL_HOST_HEADER)" && \ + echo "" && \ + echo "✅ Production URLs saved to: $$PROD_OUTPUT" && \ + echo "✅ Local URLs saved to: $$LOCAL_OUTPUT" && \ + go run compare.go "$$PROD_OUTPUT" "$$LOCAL_OUTPUT" + +# Extract PNG icons URLs +png: build create-log-dir + @echo "Extracting PNG icons URLs..." + @PROD_OUTPUT="$(LOG_DIR)/urls-prod-png.txt" && \ + LOCAL_OUTPUT="$(LOG_DIR)/urls-local-png.txt" && \ + ./$(BINARY) -url "https://hexmos.com/freedevtools/png_icons/sitemap.xml" -output "$$PROD_OUTPUT" && \ + ./$(BINARY) -url "http://127.0.0.1/freedevtools/png_icons/sitemap.xml" -output "$$LOCAL_OUTPUT" -host "$(LOCAL_HOST_HEADER)" && \ + echo "" && \ + echo "✅ Production URLs saved to: $$PROD_OUTPUT" && \ + echo "✅ Local URLs saved to: $$LOCAL_OUTPUT" && \ + go run compare.go "$$PROD_OUTPUT" "$$LOCAL_OUTPUT" + +# Extract emojis URLs +emoji: build create-log-dir + @echo "Extracting emojis URLs..." + @PROD_OUTPUT="$(LOG_DIR)/urls-prod-emoji.txt" && \ + LOCAL_OUTPUT="$(LOG_DIR)/urls-local-emoji.txt" && \ + ./$(BINARY) -url "https://hexmos.com/freedevtools/emojis/sitemap.xml" -output "$$PROD_OUTPUT" && \ + ./$(BINARY) -url "http://127.0.0.1/freedevtools/emojis/sitemap.xml" -output "$$LOCAL_OUTPUT" -host "$(LOCAL_HOST_HEADER)" && \ + echo "" && \ + echo "✅ Production URLs saved to: $$PROD_OUTPUT" && \ + echo "✅ Local URLs saved to: $$LOCAL_OUTPUT" && \ + go run compare.go "$$PROD_OUTPUT" "$$LOCAL_OUTPUT" + +# Clean output files and binary +clean: + @rm -rf logs/ urls-prod-*.txt urls-local-*.txt sitemap-prod-*.xml sitemap-local-*.xml $(BINARY) + @echo "✅ Cleaned all output files and logs" + diff --git a/frontend/scripts/sitemap-checker-v2/README.md b/frontend/scripts/sitemap-checker-v2/README.md new file mode 100644 index 0000000000..b9ee95bb49 --- /dev/null +++ b/frontend/scripts/sitemap-checker-v2/README.md @@ -0,0 +1,54 @@ +# Sitemap Checker V2 + +Simple Go program that extracts all URLs from sitemaps (recursively fetching sub-sitemaps) and saves them to text files. + +## Usage + +```bash +cd sitemap-checker-v2 +make svg # Extract SVG icons URLs +make png # Extract PNG icons URLs +make emoji # Extract emojis URLs +make clean # Remove all output files and logs +``` + +## Output Files + +All output files are saved in timestamped folders under `logs/`: +- `logs/YYYY-MM-DD_HH-MM-SS/urls-prod-.txt` - All URLs from production sitemap (one URL per line) +- `logs/YYYY-MM-DD_HH-MM-SS/urls-local-.txt` - All URLs from local sitemap (one URL per line) + +Each run creates a new timestamped folder, so you can keep a history of all extractions. + +## How It Works + +1. Fetches the main sitemap XML using Go's HTTP client +2. Parses XML to detect if it's a sitemap index or URL set +3. If it's a sitemap index (contains sub-sitemaps), recursively fetches all sub-sitemaps +4. Extracts all `` entries (ignores `` entries) +5. Saves all URLs to a simple text file (one URL per line, no XML metadata) + +## Building + +The Makefile automatically builds the Go binary when you run `make svg`, `make png`, or `make emoji`. You can also build manually: + +```bash +go build -o extract-urls extract-urls.go +``` + +## Examples + +```bash +# Extract SVG icons URLs +make svg + +# Extract PNG icons URLs +make png + +# Extract emojis URLs +make emoji + +# Clean all files +make clean +``` + diff --git a/frontend/scripts/sitemap-checker-v2/compare.go b/frontend/scripts/sitemap-checker-v2/compare.go new file mode 100644 index 0000000000..e240d195a1 --- /dev/null +++ b/frontend/scripts/sitemap-checker-v2/compare.go @@ -0,0 +1,187 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "sort" + "strings" +) + +func loadUrlsFromFile(filename string) ([]string, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var urls []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + url := strings.TrimSpace(scanner.Text()) + if url != "" { + urls = append(urls, url) + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return urls, nil +} + +func normalizeUrl(url string) string { + // Normalize localhost URLs to production URLs for comparison + url = strings.Replace(url, "http://localhost:4321", "https://hexmos.com", 1) + url = strings.Replace(url, "http://127.0.0.1", "https://hexmos.com", 1) + return url +} + +func compareUrls(prodFile, localFile string) error { + prodUrls, err := loadUrlsFromFile(prodFile) + if err != nil { + return fmt.Errorf("failed to load prod URLs: %v", err) + } + + localUrls, err := loadUrlsFromFile(localFile) + if err != nil { + return fmt.Errorf("failed to load local URLs: %v", err) + } + + // Normalize local URLs for comparison + normalizedLocalUrls := make(map[string]string) // normalized -> original + for _, url := range localUrls { + normalized := normalizeUrl(url) + normalizedLocalUrls[normalized] = url + } + + // Create prod URL map + prodUrlMap := make(map[string]bool) + for _, url := range prodUrls { + prodUrlMap[url] = true + } + + // Find missing in local and extra in local + var missingInLocal []string + var extraInLocal []string + + // Check prod URLs - missing in local + for _, prodUrl := range prodUrls { + if _, exists := normalizedLocalUrls[prodUrl]; !exists { + missingInLocal = append(missingInLocal, prodUrl) + } + } + + // Check local URLs - extra in local + for normalized, original := range normalizedLocalUrls { + if !prodUrlMap[normalized] { + extraInLocal = append(extraInLocal, original) + } + } + + // Sort for consistent output + sort.Strings(missingInLocal) + sort.Strings(extraInLocal) + + // Print results + fmt.Println("\n" + strings.Repeat("=", 70)) + fmt.Println("SITEMAP COMPARISON RESULTS") + fmt.Println(strings.Repeat("=", 70)) + fmt.Printf("Production URLs: %d\n", len(prodUrls)) + fmt.Printf("Local URLs: %d\n", len(localUrls)) + fmt.Println(strings.Repeat("-", 70)) + fmt.Printf("Missing in Local: %d URLs\n", len(missingInLocal)) + fmt.Printf("Extra in Local: %d URLs\n", len(extraInLocal)) + fmt.Println(strings.Repeat("=", 70)) + + if len(missingInLocal) > 0 { + fmt.Println("\n📋 URLs Missing in Local:") + fmt.Println(strings.Repeat("-", 70)) + for i, url := range missingInLocal { + if i >= 20 { + fmt.Printf("... and %d more URLs missing in local\n", len(missingInLocal)-20) + break + } + fmt.Printf(" %s\n", url) + } + } + + if len(extraInLocal) > 0 { + fmt.Println("\n📋 URLs Extra in Local:") + fmt.Println(strings.Repeat("-", 70)) + for i, url := range extraInLocal { + if i >= 20 { + fmt.Printf("... and %d more URLs extra in local\n", len(extraInLocal)-20) + break + } + fmt.Printf(" %s\n", url) + } + } + + if len(missingInLocal) == 0 && len(extraInLocal) == 0 { + fmt.Println("\n✅ Perfect match! All URLs are identical.") + } + + fmt.Println() + + // Save missing and extra URLs to files + // Extract directory from prodFile path + prodDir := "" + if lastSlash := strings.LastIndex(prodFile, "/"); lastSlash != -1 { + prodDir = prodFile[:lastSlash+1] + } + + // Determine file type from filename + fileType := "svg" + if strings.Contains(prodFile, "png") { + fileType = "png" + } else if strings.Contains(prodFile, "emoji") { + fileType = "emoji" + } + + missingFile := fmt.Sprintf("%slocal-missing-%s.txt", prodDir, fileType) + extraFile := fmt.Sprintf("%slocal-extra-%s.txt", prodDir, fileType) + + // Write missing URLs + if len(missingInLocal) > 0 { + missingF, err := os.Create(missingFile) + if err == nil { + for _, url := range missingInLocal { + fmt.Fprintln(missingF, url) + } + missingF.Close() + fmt.Printf("📄 Missing URLs saved to: %s\n", missingFile) + } + } + + // Write extra URLs + if len(extraInLocal) > 0 { + extraF, err := os.Create(extraFile) + if err == nil { + for _, url := range extraInLocal { + fmt.Fprintln(extraF, url) + } + extraF.Close() + fmt.Printf("📄 Extra URLs saved to: %s\n", extraFile) + } + } + + return nil +} + +func main() { + if len(os.Args) < 3 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + + prodFile := os.Args[1] + localFile := os.Args[2] + + if err := compareUrls(prodFile, localFile); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + diff --git a/frontend/scripts/sitemap-checker-v2/extract-urls b/frontend/scripts/sitemap-checker-v2/extract-urls new file mode 100755 index 0000000000..aa95bdbae2 Binary files /dev/null and b/frontend/scripts/sitemap-checker-v2/extract-urls differ diff --git a/frontend/scripts/sitemap-checker-v2/extract-urls.go b/frontend/scripts/sitemap-checker-v2/extract-urls.go new file mode 100644 index 0000000000..a1f8535b12 --- /dev/null +++ b/frontend/scripts/sitemap-checker-v2/extract-urls.go @@ -0,0 +1,149 @@ +package main + +import ( + "encoding/xml" + "flag" + "fmt" + "io" + "net/http" + "os" +) + +type SitemapIndex struct { + XMLName xml.Name `xml:"sitemapindex"` + Sitemaps []struct { + Loc string `xml:"loc"` + } `xml:"sitemap"` +} + +type UrlSet struct { + XMLName xml.Name `xml:"urlset"` + URLs []struct { + Loc string `xml:"loc"` + } `xml:"url"` +} + +func fetchSitemap(url, hostHeader string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + if hostHeader != "" { + req.Host = hostHeader + req.Header.Set("Host", hostHeader) + } + + req.Header.Set("User-Agent", "SitemapChecker/1.0") + req.Close = true + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} + +func extractUrlsFromXML(data []byte) ([]string, []string) { + var urls []string + var subSitemaps []string + + // Try parsing as UrlSet first + var urlset UrlSet + if err := xml.Unmarshal(data, &urlset); err == nil && len(urlset.URLs) > 0 { + for _, u := range urlset.URLs { + if u.Loc != "" { + urls = append(urls, u.Loc) + } + } + return urls, nil + } + + // Try parsing as SitemapIndex + var sitemapIndex SitemapIndex + if err := xml.Unmarshal(data, &sitemapIndex); err == nil && len(sitemapIndex.Sitemaps) > 0 { + for _, sm := range sitemapIndex.Sitemaps { + if sm.Loc != "" { + subSitemaps = append(subSitemaps, sm.Loc) + } + } + return nil, subSitemaps + } + + return urls, nil +} + +func extractAllUrls(sitemapUrl, hostHeader string, visited map[string]bool) ([]string, error) { + if visited == nil { + visited = make(map[string]bool) + } + + // Prevent infinite loops + if visited[sitemapUrl] { + return nil, nil + } + visited[sitemapUrl] = true + + var allUrls []string + + // Fetch sitemap + data, err := fetchSitemap(sitemapUrl, hostHeader) + if err != nil { + return nil, fmt.Errorf("failed to fetch %s: %v", sitemapUrl, err) + } + + // Extract URLs and sub-sitemaps + urls, subSitemaps := extractUrlsFromXML(data) + allUrls = append(allUrls, urls...) + + // Recursively fetch sub-sitemaps + for _, subUrl := range subSitemaps { + subUrls, err := extractAllUrls(subUrl, hostHeader, visited) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to fetch sub-sitemap %s: %v\n", subUrl, err) + continue + } + allUrls = append(allUrls, subUrls...) + } + + return allUrls, nil +} + +func main() { + var sitemapUrl, outputFile, hostHeader string + + flag.StringVar(&sitemapUrl, "url", "", "Sitemap URL to extract URLs from") + flag.StringVar(&outputFile, "output", "", "Output file to save URLs (one per line)") + flag.StringVar(&hostHeader, "host", "", "Host header for local requests (optional)") + flag.Parse() + + if sitemapUrl == "" || outputFile == "" { + fmt.Println("Usage: extract-urls -url -output [-host ]") + os.Exit(1) + } + + // Extract all URLs recursively + urls, err := extractAllUrls(sitemapUrl, hostHeader, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + // Write URLs to file (one per line) + file, err := os.Create(outputFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err) + os.Exit(1) + } + defer file.Close() + + for _, url := range urls { + fmt.Fprintln(file, url) + } + + fmt.Printf("✅ Extracted %d URLs to: %s\n", len(urls), outputFile) +} + diff --git a/frontend/scripts/sitemap-checker/Makefile b/frontend/scripts/sitemap-checker/Makefile index 038b939625..2f119099ae 100644 --- a/frontend/scripts/sitemap-checker/Makefile +++ b/frontend/scripts/sitemap-checker/Makefile @@ -14,6 +14,7 @@ MAXPAGES ?= 0 OUTPUT ?= pdf HEAD ?= false COMPARE_PROD ?= false +LOG_FILE ?= # ----------------------------- # Targets @@ -45,7 +46,8 @@ run-bin: build --maxPages=$(MAXPAGES) \ --output=$(OUTPUT) \ --head=$(HEAD) \ - --compare-prod=$(COMPARE_PROD) + --compare-prod=$(COMPARE_PROD) \ + --log-file=$(LOG_FILE) # Clean the binary clean: @@ -59,4 +61,12 @@ apple-emojis: tldr: make run-bin SITEMAP=http://localhost:4321/freedevtools/tldr/sitemap.xml COMPARE_PROD=true + +svg: + @mkdir -p logs + @make run-bin SITEMAP=http://127.0.0.1/freedevtools/svg_icons/sitemap.xml COMPARE_PROD=true LOG_FILE=logs/sitemap-checker-$$(date +%Y%m%d-%H%M%S).log + +png: + @mkdir -p logs + @make run-bin SITEMAP=http://127.0.0.1/freedevtools/png_icons/sitemap.xml COMPARE_PROD=true LOG_FILE=logs/sitemap-checker-$$(date +%Y%m%d-%H%M%S).log \ No newline at end of file diff --git a/frontend/scripts/sitemap-checker/checks.go b/frontend/scripts/sitemap-checker/checks.go index 170ac725b1..64dcc61ff0 100644 --- a/frontend/scripts/sitemap-checker/checks.go +++ b/frontend/scripts/sitemap-checker/checks.go @@ -28,7 +28,11 @@ func checkUrl(url string, headOnly bool) UrlResult { result.Issues = append(result.Issues, "Fetch failed") return result } - defer resp.Body.Close() + defer func() { + // Always drain the body to ensure connection is properly closed + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + }() result.Status = resp.StatusCode diff --git a/frontend/scripts/sitemap-checker/compare.go b/frontend/scripts/sitemap-checker/compare.go index f5c264e7c4..8be5f70db6 100644 --- a/frontend/scripts/sitemap-checker/compare.go +++ b/frontend/scripts/sitemap-checker/compare.go @@ -2,8 +2,8 @@ package main import ( "encoding/xml" - "fmt" "io" + "net/http" "sort" "strings" ) @@ -15,10 +15,73 @@ type SitemapComparison struct { LocalTotal int } +// fetchWithHostHeader makes a GET request with the appropriate Host header +func fetchWithHostHeader(url string) (*http.Response, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", "GoSitemapChecker/1.0") + // Disable keep-alive for this request to avoid connection reuse issues + req.Close = true + + // Set Host header for nginx routing + hostHeader := getHostHeader(url) + if hostHeader != "" { + req.Host = hostHeader + req.Header.Set("Host", hostHeader) + } + + return client.Do(req) +} + +// normalizeUrlToOrigin converts a URL to use the specified origin +// If URL is already absolute, extract path and prepend origin +// If URL is relative, prepend origin +func normalizeUrlToOrigin(url, origin string) string { + if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { + // Extract path from absolute URL + if strings.Contains(url, "hexmos.com") { + path := strings.Replace(url, "https://hexmos.com", "", 1) + return origin + path + } else if strings.Contains(url, "127.0.0.1") { + path := strings.Replace(url, "http://127.0.0.1", "", 1) + return origin + path + } else if strings.Contains(url, "localhost:4321") { + path := strings.Replace(url, "http://localhost:4321", "", 1) + return origin + path + } + // Unknown domain, try to extract path + parts := strings.SplitN(url, "/", 4) + if len(parts) >= 4 { + return origin + "/" + parts[3] + } + return url + } + // Relative URL + if strings.HasPrefix(url, "/") { + return origin + url + } + return origin + "/" + url +} + +// getOriginFromUrl extracts the origin (scheme + host) from a URL +func getOriginFromUrl(url string) string { + if strings.Contains(url, "127.0.0.1") { + return "http://127.0.0.1" + } else if strings.Contains(url, "localhost:4321") { + return "http://localhost:4321" + } else if strings.Contains(url, "hexmos.com") { + return "https://hexmos.com" + } + // Default fallback + return "http://127.0.0.1" +} + func fetchRawSitemapUrls(sitemapUrl string) []string { - resp, err := client.Get(sitemapUrl) + resp, err := fetchWithHostHeader(sitemapUrl) if err != nil { - fmt.Println("Failed to load sitemap for comparison:", err) + logPrintln("Failed to load sitemap for comparison:", err) return []string{} } defer resp.Body.Close() @@ -27,6 +90,9 @@ func fetchRawSitemapUrls(sitemapUrl string) []string { var urls []string var urlset UrlSet var smIndex SitemapIndex + + // Get origin from input URL to preserve it for sub-sitemaps + origin := getOriginFromUrl(sitemapUrl) if err := xml.Unmarshal(body, &urlset); err == nil && len(urlset.URLs) > 0 { for _, u := range urlset.URLs { @@ -34,7 +100,9 @@ func fetchRawSitemapUrls(sitemapUrl string) []string { } } else if err := xml.Unmarshal(body, &smIndex); err == nil && len(smIndex.Sitemaps) > 0 { for _, sm := range smIndex.Sitemaps { - subResp, err := client.Get(sm.Loc) + // Normalize sub-sitemap URL to use same origin as input + normalizedSubUrl := normalizeUrlToOrigin(sm.Loc, origin) + subResp, err := fetchWithHostHeader(normalizedSubUrl) if err != nil { continue } @@ -69,7 +137,10 @@ func compareSitemaps(prodUrls, localUrls []string) *SitemapComparison { localMap := make(map[string]bool) for _, u := range localUrls { // Normalize local URL to prod domain for comparison - normalized := strings.Replace(u, "http://localhost:4321", "https://hexmos.com", 1) + // Handle both 127.0.0.1 and localhost:4321 + normalized := u + normalized = strings.Replace(normalized, "http://127.0.0.1", "https://hexmos.com", 1) + normalized = strings.Replace(normalized, "http://localhost:4321", "https://hexmos.com", 1) localMap[normalized] = true if !prodMap[normalized] { diff --git a/frontend/scripts/sitemap-checker/fetcher.go b/frontend/scripts/sitemap-checker/fetcher.go index 9ce76e3f04..108c4d7035 100644 --- a/frontend/scripts/sitemap-checker/fetcher.go +++ b/frontend/scripts/sitemap-checker/fetcher.go @@ -1,7 +1,9 @@ package main import ( + "io" "net/http" + "net/url" "strings" // "time" ) @@ -13,6 +15,26 @@ func ToOfflineUrl(url string) string { return url } +// getHostHeader determines the Host header value based on the request URL and mode +func getHostHeader(requestUrl string) string { + parsedUrl, err := url.Parse(requestUrl) + if err != nil { + return "" + } + + host := parsedUrl.Hostname() + + // If requesting localhost or 127.0.0.1, set Host header based on mode + if host == "localhost" || host == "127.0.0.1" || strings.HasPrefix(host, "127.0.0.1") { + if mode == "local" { + return "hexmos-local.com" + } + return "hexmos.com" + } + + // For other hosts, use the actual hostname + return host +} func fetchText(url string, headOnly bool) (*http.Response, error) { var req *http.Request @@ -26,6 +48,16 @@ func fetchText(url string, headOnly bool) (*http.Response, error) { return nil, err } req.Header.Set("User-Agent", "GoSitemapChecker/1.0") + // Disable keep-alive for this request to avoid connection reuse issues + req.Close = true + + // Set Host header for nginx routing + hostHeader := getHostHeader(url) + if hostHeader != "" { + req.Host = hostHeader + req.Header.Set("Host", hostHeader) + } + return client.Do(req) } @@ -33,10 +65,22 @@ func fetchText(url string, headOnly bool) (*http.Response, error) { func validateCanonical(url string) bool { req, _ := http.NewRequest("HEAD", url, nil) req.Header.Set("User-Agent", "GoSitemapChecker/1.0") + // Disable keep-alive for this request + req.Close = true + + // Set Host header for nginx routing + hostHeader := getHostHeader(url) + if hostHeader != "" { + req.Host = hostHeader + req.Header.Set("Host", hostHeader) + } + resp, err := client.Do(req) if err != nil { return false } defer resp.Body.Close() + // Drain the body even for HEAD requests to ensure connection is properly closed + io.Copy(io.Discard, resp.Body) return resp.StatusCode == 200 } diff --git a/frontend/scripts/sitemap-checker/main.go b/frontend/scripts/sitemap-checker/main.go index 9271095687..74c7404cb0 100644 --- a/frontend/scripts/sitemap-checker/main.go +++ b/frontend/scripts/sitemap-checker/main.go @@ -20,6 +20,8 @@ var ( mode string maxPages int client *http.Client + logWriter io.Writer + logFile *os.File ) type UrlSet struct { @@ -44,10 +46,37 @@ type UrlResult struct { +// logPrint writes to both stdout and log file (if enabled) +func logPrint(format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + fmt.Print(msg) + if logWriter != nil { + logWriter.Write([]byte(msg)) + } +} + +// logPrintf writes formatted string to both stdout and log file (if enabled) +func logPrintf(format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + fmt.Print(msg) + if logWriter != nil { + logWriter.Write([]byte(msg)) + } +} + +// logPrintln writes to both stdout and log file (if enabled) +func logPrintln(args ...interface{}) { + msg := fmt.Sprintln(args...) + fmt.Print(msg) + if logWriter != nil { + logWriter.Write([]byte(msg)) + } +} + func main() { runtime.GOMAXPROCS(runtime.NumCPU()) - var sitemapUrl, inputJSON string + var sitemapUrl, inputJSON, logFilePath string var outputFormat string var useHead bool var compareProd bool @@ -60,9 +89,27 @@ func main() { flag.BoolVar(&useHead, "head", false, "Use HEAD requests only (check 404/200 without reading full body)") flag.StringVar(&outputFormat, "output", "pdf", "Output format: pdf, json, or both") flag.BoolVar(&compareProd, "compare-prod", false, "Compare local sitemap with production sitemap") + flag.StringVar(&logFilePath, "log-file", "", "Log file path (optional, logs will also go to stdout)") flag.Parse() + // Setup log file if specified + if logFilePath != "" { + var err error + logFile, err = os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Printf("Failed to open log file %s: %v\n", logFilePath, err) + os.Exit(1) + } + defer logFile.Close() + // Write to both stdout and log file + logWriter = io.MultiWriter(logFile) + logPrintln("=== Sitemap Checker Started ===") + logPrintln("Log file:", logFilePath) + } else { + logWriter = nil + } + client = &http.Client{ Timeout: 1500 * time.Second, Transport: &http.Transport{ @@ -70,6 +117,7 @@ func main() { MaxIdleConnsPerHost: 4000, MaxConnsPerHost: 4000, IdleConnTimeout: 90 * time.Second, + DisableKeepAlives: false, // Keep keep-alive but ensure we read full body }, } @@ -77,32 +125,59 @@ func main() { var comparison *SitemapComparison if compareProd && sitemapUrl != "" { - fmt.Println("--- Starting Sitemap Comparison ---") - // 1. Determine Prod and Local Sitemap URLs + logPrintln("--- Starting Sitemap Comparison ---") + // Production always uses hexmos.com + // Local preserves the input origin (127.0.0.1 or localhost:4321) + var prodSitemapUrl, localSitemapUrl string - - if strings.Contains(sitemapUrl, "localhost") { - // Input is Local, derive Prod + + // Extract path from input URL + var path string + if strings.Contains(sitemapUrl, "127.0.0.1") { + path = strings.Replace(sitemapUrl, "http://127.0.0.1", "", 1) localSitemapUrl = sitemapUrl - prodSitemapUrl = strings.Replace(sitemapUrl, "http://localhost:4321", "https://hexmos.com", 1) + } else if strings.Contains(sitemapUrl, "localhost:4321") { + path = strings.Replace(sitemapUrl, "http://localhost:4321", "", 1) + localSitemapUrl = sitemapUrl + } else if strings.Contains(sitemapUrl, "hexmos.com") { + path = strings.Replace(sitemapUrl, "https://hexmos.com", "", 1) + // If input is prod, derive local as 127.0.0.1 + localSitemapUrl = "http://127.0.0.1" + path } else { - // Input is Prod (or other), derive Local - prodSitemapUrl = sitemapUrl - localSitemapUrl = strings.Replace(sitemapUrl, "https://hexmos.com", "http://localhost:4321", 1) + // Unknown, assume it's local + localSitemapUrl = sitemapUrl + if strings.HasPrefix(sitemapUrl, "http://") { + parts := strings.SplitN(sitemapUrl, "/", 4) + if len(parts) >= 4 { + path = "/" + parts[3] + } else { + path = strings.TrimPrefix(sitemapUrl, "http://") + if idx := strings.Index(path, "/"); idx != -1 { + path = path[idx:] + } else { + path = "/" + } + } + } else { + path = sitemapUrl + } } + + // Production always uses hexmos.com + prodSitemapUrl = "https://hexmos.com" + path - fmt.Println("Fetching Production URLs from:", prodSitemapUrl) + logPrintln("Fetching Production URLs from:", prodSitemapUrl) prodUrls := fetchRawSitemapUrls(prodSitemapUrl) - fmt.Printf("Found %d URLs in Production\n", len(prodUrls)) + logPrintf("Found %d URLs in Production\n", len(prodUrls)) - fmt.Println("Fetching Local URLs from:", localSitemapUrl) + logPrintln("Fetching Local URLs from:", localSitemapUrl) localUrls := fetchRawSitemapUrls(localSitemapUrl) - fmt.Printf("Found %d URLs in Local\n", len(localUrls)) + logPrintf("Found %d URLs in Local\n", len(localUrls)) // 3. Compare comparison = compareSitemaps(prodUrls, localUrls) - fmt.Printf("Comparison Result: %d Missing in Local, %d Extra in Local\n", len(comparison.MissingInLocal), len(comparison.ExtraInLocal)) - fmt.Println("--- Comparison Done ---") + logPrintf("Comparison Result: %d Missing in Local, %d Extra in Local\n", len(comparison.MissingInLocal), len(comparison.ExtraInLocal)) + logPrintln("--- Comparison Done ---") } @@ -112,7 +187,7 @@ func main() { } else if inputJSON != "" { urls = loadUrlsFromJSON(inputJSON) } else { - fmt.Println("Usage: go run main.go --sitemap= OR --input= [--concurrency=200] [--mode=prod|local] [--maxPages=10] [--output=pdf|json|both] [--compare-prod]") + logPrintln("Usage: go run main.go --sitemap= OR --input= [--concurrency=200] [--mode=prod|local] [--maxPages=10] [--output=pdf|json|both] [--compare-prod] [--log-file=]") os.Exit(1) } @@ -120,7 +195,7 @@ func main() { urls = urls[:maxPages] } - fmt.Printf("Total URLs to check: %d\n", len(urls)) + logPrintf("Total URLs to check: %d\n", len(urls)) jobs := make(chan string, len(urls)) results := make(chan UrlResult, len(urls)) @@ -143,13 +218,13 @@ func main() { go func() { for { done := atomic.LoadInt32(&completed) - fmt.Printf("\rProgress: %d/%d URLs checked", done, totalUrls) + logPrintf("\rProgress: %d/%d URLs checked", done, totalUrls) if int(done) >= totalUrls { break } time.Sleep(500 * time.Millisecond) } - fmt.Println() + logPrintln() }() for _, u := range urls { @@ -171,13 +246,17 @@ func main() { if outputFormat == "pdf" || outputFormat == "both" { pdfName := fmt.Sprintf("sitemap_report_%s.pdf", timestamp) generatePDF(all, pdfName, comparison) - fmt.Println("✅ PDF saved as", pdfName) + logPrintln("✅ PDF saved as", pdfName) } if outputFormat == "json" || outputFormat == "both" { jsonName := fmt.Sprintf("sitemap_report_%s.json", timestamp) saveJSON(all, jsonName) } + + if logFile != nil { + logPrintln("=== Sitemap Checker Completed ===") + } } // ------------------------- @@ -185,10 +264,10 @@ func main() { // ------------------------- func loadUrlsFromSitemap(sitemapUrl string) []string { - fmt.Println("Fetching sitemap:", sitemapUrl) - resp, err := client.Get(sitemapUrl) + logPrintln("Fetching sitemap:", sitemapUrl) + resp, err := fetchWithHostHeader(sitemapUrl) if err != nil { - fmt.Println("Failed to load sitemap:", err) + logPrintln("Failed to load sitemap:", err) os.Exit(1) } defer resp.Body.Close() @@ -197,21 +276,28 @@ func loadUrlsFromSitemap(sitemapUrl string) []string { var urls []string var urlset UrlSet var smIndex SitemapIndex + + // Get origin from input URL to preserve it for sub-sitemaps and URLs + origin := getOriginFromUrl(sitemapUrl) // Try parsing as UrlSet if err := xml.Unmarshal(body, &urlset); err == nil && len(urlset.URLs) > 0 { - fmt.Printf("Found %d URLs in sitemap: %s\n", len(urlset.URLs), sitemapUrl) + logPrintf("Found %d URLs in sitemap: %s\n", len(urlset.URLs), sitemapUrl) for _, u := range urlset.URLs { - urls = append(urls, ToOfflineUrl(u.Loc)) + // Normalize URL to use same origin as input + normalizedUrl := normalizeUrlToOrigin(u.Loc, origin) + urls = append(urls, ToOfflineUrl(normalizedUrl)) } } else if err := xml.Unmarshal(body, &smIndex); err == nil && len(smIndex.Sitemaps) > 0 { // Try parsing as SitemapIndex - fmt.Printf("Found Sitemap Index with %d sub-sitemaps: %s\n", len(smIndex.Sitemaps), sitemapUrl) + logPrintf("Found Sitemap Index with %d sub-sitemaps: %s\n", len(smIndex.Sitemaps), sitemapUrl) for _, sm := range smIndex.Sitemaps { - fmt.Println(" -> Fetching sub-sitemap:", sm.Loc) - subResp, err := client.Get(sm.Loc) + // Normalize sub-sitemap URL to use same origin as input + normalizedSubUrl := normalizeUrlToOrigin(sm.Loc, origin) + logPrintln(" -> Fetching sub-sitemap:", normalizedSubUrl) + subResp, err := fetchWithHostHeader(normalizedSubUrl) if err != nil { - fmt.Printf(" [ERROR] Failed to fetch sub-sitemap %s: %v\n", sm.Loc, err) + logPrintf(" [ERROR] Failed to fetch sub-sitemap %s: %v\n", normalizedSubUrl, err) continue } @@ -220,16 +306,18 @@ func loadUrlsFromSitemap(sitemapUrl string) []string { var subUrlset UrlSet if err := xml.Unmarshal(data, &subUrlset); err == nil { - fmt.Printf(" Found %d URLs in sub-sitemap\n", len(subUrlset.URLs)) + logPrintf(" Found %d URLs in sub-sitemap\n", len(subUrlset.URLs)) for _, u := range subUrlset.URLs { - urls = append(urls, ToOfflineUrl(u.Loc)) + // Normalize URL to use same origin as input + normalizedUrl := normalizeUrlToOrigin(u.Loc, origin) + urls = append(urls, ToOfflineUrl(normalizedUrl)) } } else { - fmt.Printf(" [ERROR] Failed to parse sub-sitemap %s: %v\n", sm.Loc, err) + logPrintf(" [ERROR] Failed to parse sub-sitemap %s: %v\n", normalizedSubUrl, err) } } } else { - fmt.Println("Warning: No URLs or Sub-Sitemaps found in:", sitemapUrl) + logPrintln("Warning: No URLs or Sub-Sitemaps found in:", sitemapUrl) } return urls @@ -238,12 +326,12 @@ func loadUrlsFromSitemap(sitemapUrl string) []string { func loadUrlsFromJSON(file string) []string { data, err := os.ReadFile(file) if err != nil { - fmt.Println("Failed to read JSON file:", err) + logPrintln("Failed to read JSON file:", err) os.Exit(1) } var urls []string if err := json.Unmarshal(data, &urls); err != nil { - fmt.Println("Invalid JSON file format:", err) + logPrintln("Invalid JSON file format:", err) os.Exit(1) } @@ -265,12 +353,12 @@ func loadUrlsFromJSON(file string) []string { func saveJSON(results []UrlResult, filename string) { data, err := json.MarshalIndent(results, "", " ") if err != nil { - fmt.Println("Failed to marshal JSON:", err) + logPrintln("Failed to marshal JSON:", err) return } if err := os.WriteFile(filename, data, 0644); err != nil { - fmt.Println("Failed to write JSON file:", err) + logPrintln("Failed to write JSON file:", err) return } - fmt.Println("✅ JSON saved as", filename) + logPrintln("✅ JSON saved as", filename) } diff --git a/frontend/scripts/sitemap-checker/pdf.go b/frontend/scripts/sitemap-checker/pdf.go index 60650cab1f..e4f92cd4c5 100644 --- a/frontend/scripts/sitemap-checker/pdf.go +++ b/frontend/scripts/sitemap-checker/pdf.go @@ -209,8 +209,8 @@ func generatePDF(results []UrlResult, filename string, comparison *SitemapCompar err := pdf.OutputFileAndClose(filename) if err != nil { - fmt.Println("Error saving PDF:", err) + logPrintln("Error saving PDF:", err) } else { - fmt.Println("✅ PDF report saved as", filename) + logPrintln("✅ PDF report saved as", filename) } } diff --git a/frontend/scripts/ssr/SSR_CONVERSION_GUIDE.md b/frontend/scripts/ssr/SSR_CONVERSION_GUIDE.md new file mode 100644 index 0000000000..27e090443a --- /dev/null +++ b/frontend/scripts/ssr/SSR_CONVERSION_GUIDE.md @@ -0,0 +1,909 @@ +# SSR Mode Conversion Guide + +This document details the process of converting Astro static pages to SSR (Server-Side Rendering) mode, specifically for the TLDR section. Use this guide when converting other sections (MCP, SVG icons, etc.) to SSR mode. + +## Table of Contents + +1. [Overview](#overview) +2. [Key Changes Required](#key-changes-required) +3. [Route Structure Changes](#route-structure-changes) +4. [Removing Static Generation Code](#removing-static-generation-code) +5. [Content Collection Usage](#content-collection-usage) +6. [Middleware for Route Priority](#middleware-for-route-priority) +7. [Step-by-Step Conversion Process](#step-by-step-conversion-process) +8. [Common Issues and Solutions](#common-issues-and-solutions) + +--- + +## Overview + +### What Changed + +- **Before**: Pages were pre-rendered at build time using `getStaticPaths()` +- **After**: Pages are rendered on-demand at request time (SSR mode) + +### Why SSR? + +- Dynamic content that changes frequently +- Large number of pages that would be slow to pre-render +- Need for real-time data fetching + +--- + +## Key Changes Required + +### 1. Remove `getStaticPaths()` + +**Static Mode (Before):** + +```typescript +export async function getStaticPaths() { + const items = await getCollection('collection'); + return items.map((item) => ({ + params: { id: item.id }, + props: { data: item.data }, + })); +} + +const { data } = Astro.props; // Data from getStaticPaths +``` + +**SSR Mode (After):** + +```typescript +export const prerender = false; // Explicitly disable prerendering + +const { id } = Astro.params; // Get params directly +const entry = await getCollection('collection'); +const item = entry.find((e) => e.id === id); // Fetch data directly +``` + +### 2. Fetch Data Directly in Component + +In SSR mode, you cannot use `Astro.props` from `getStaticPaths()`. Instead: + +- Fetch data directly using `getCollection()` or other data sources +- Use `Astro.params` to get route parameters +- Handle data fetching asynchronously in the frontmatter + +--- + +## Route Structure Changes + +### Problem: Route Collisions in SSR + +In SSR mode, Astro cannot have ambiguous dynamic routes. For example: + +- `/tldr/[platform]/[command].astro` +- `/tldr/[platform]/[page].astro` + +Both match the pattern `/tldr/[platform]/[something]`, causing collisions. + +### Solution: Consolidate Routes + +**Before (2 separate files):** + +``` +src/pages/tldr/[platform]/ + ├── [command].astro (handles /tldr/platform/command) + └── [page].astro (handles /tldr/platform/2) +``` + +**After (1 consolidated file):** + +``` +src/pages/tldr/[platform]/ + └── [slug].astro (handles both /tldr/platform/command AND /tldr/platform/2) +``` + +### Implementation Pattern + +```typescript +// src/pages/tldr/[platform]/[slug].astro +--- +const { platform, slug } = Astro.params; + +// Check if slug is numeric (pagination) or a string (command) +const isNumericPage = slug && /^\d+$/.test(slug); +const pageNumber = isNumericPage ? parseInt(slug, 10) : null; +const command = !isNumericPage ? slug : null; + +if (pageNumber !== null) { + // Handle pagination route + // ... pagination logic +} else if (command) { + // Handle command route + // ... command logic +} +--- +``` + +--- + +## Removing Static Generation Code + +### Files to Remove/Modify + +1. **Remove `getStaticPaths()` functions** from all SSR pages +2. **Remove `export const prerender = true`** (or set to `false`) +3. **Remove utility functions** that generate static paths (if only used for static generation) + +### Example: Before and After + +**Before (Static):** + +```typescript +// src/pages/tldr/[platform]/[command].astro +export async function getStaticPaths() { + const entries = await getCollection('tldr'); + return entries.map((entry) => ({ + params: { platform: '...', command: '...' }, + })); +} + +const { platform, command } = Astro.params; +const { data } = Astro.props; // ❌ Won't work in SSR +``` + +**After (SSR):** + +```typescript +// src/pages/tldr/[platform]/[slug].astro +export const prerender = false; // ✅ Explicitly SSR + +const { platform, slug } = Astro.params; +const entries = await getCollection('tldr'); // ✅ Fetch directly +const entry = entries.find(/* ... */); +``` + +--- + +## Content Collection Usage + +### How Content Collections Work in SSR + +Content collections are defined in `src/content.config.ts` and work the same in both static and SSR modes: + +```typescript +// src/content.config.ts +const tldr = defineCollection({ + loader: glob({ + pattern: '**/*.md', + base: 'data/tldr', + }), + schema: z.object({ + title: z.string(), + description: z.string(), + // ... other fields + }), +}); +``` + +### Accessing Collections in SSR + +```typescript +import { getCollection } from 'astro:content'; + +// Get all entries +const allEntries = await getCollection('tldr'); + +// Filter entries +const platformEntries = allEntries.filter((entry) => { + const pathParts = entry.id.split('/'); + return pathParts[pathParts.length - 2] === platform; +}); + +// Access entry data (validated by schema) +const title = entry.data.title; // ✅ Type-safe +const description = entry.data.description; // ✅ Type-safe +const keywords = entry.data.keywords; // ✅ Optional, type-safe +``` + +### Rendering Content + +```typescript +import { render } from 'astro:content'; + +const { Content } = await render(entry); +// Use in template +``` + +--- + +## Middleware for Route Priority + +### Problem: Route Priority in SSR + +In SSR mode, route priority doesn't always work as expected. For example: + +- `/tldr/[page].astro` might match `/tldr/adb/` before `/tldr/[platform]/index.astro` + +### Solution: Handle in Route File (Recommended) + +**Avoid middleware rewrites** - they can cause redirect loops. Instead, handle route priority directly in the route file that matches first: + +```typescript +// src/pages/tldr/[page].astro +--- +const { page } = Astro.params; +const urlPath = Astro.url.pathname; + +// Early return if no page param +if (!page) { + return new Response(null, { status: 404 }); +} + +// Check if page param is numeric +if (!/^\d+$/.test(page)) { + // If not numeric, it might be a platform name + // Redirect to add trailing slash if missing (BEFORE checking platform) + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + const allPlatforms = await getAllTldrPlatforms(); + const isPlatform = allPlatforms.some((p) => p.name === page); + + if (isPlatform) { + // This is a platform index route - render it here + // This is a workaround for route priority not working as expected + const platform = page!; + const allCommands = await getTldrPlatformCommands(platform); + // ... render platform index content + } else { + // Not a valid platform - 404 + return new Response(null, { status: 404 }); + } +} else { + // Handle pagination route + // ... +} +--- +``` + +### Alternative: Minimal Middleware (If Needed) + +If you must use middleware, keep it simple and avoid rewrites that conflict with route handling: + +```typescript +// src/middleware.ts +import type { MiddlewareHandler } from 'astro'; + +// Minimal middleware - just pass through +// Route files handle their own logic to avoid conflicts +export const onRequest: MiddlewareHandler = async (context, next) => { + return next(); +}; +``` + +### Why Avoid Middleware Rewrites? + +1. **Redirect Loops**: Middleware rewrites can conflict with route file redirects, causing infinite loops +2. **Route Priority**: Astro's route matching happens after middleware, so rewrites may not work as expected +3. **Complexity**: Handling logic in the route file is simpler and more maintainable +4. **Performance**: Avoiding middleware lookups reduces overhead on every request + +--- + +## Step-by-Step Conversion Process + +### Step 1: Identify Route Collisions + +Check for routes that could match the same URL pattern: + +```bash +# Look for conflicting dynamic routes +find src/pages/section -name "*.astro" | grep -E "\[.*\]" +``` + +Common collisions: + +- `[page].astro` vs `[category]/index.astro` +- `[id].astro` vs `[slug].astro` +- `[name].astro` vs `[category]/[name].astro` + +### Step 2: Consolidate Conflicting Routes + +**Option A: Merge into single route with logic** + +```typescript +// [slug].astro - handles both cases +const isNumeric = /^\d+$/.test(slug); +if (isNumeric) { + // Handle pagination +} else { + // Handle content +} +``` + +**Option B: Use different path structures** + +``` +Before: /section/[page].astro and /section/[category]/index.astro +After: /section/page/[page].astro and /section/[category]/index.astro +``` + +### Step 3: Remove Static Generation Code + +For each page file: + +1. **Add SSR flag:** + + ```typescript + export const prerender = false; + ``` + +2. **Remove `getStaticPaths()`:** + + ```typescript + // ❌ Remove this + export async function getStaticPaths() { ... } + ``` + +3. **Replace `Astro.props` with direct fetching:** + + ```typescript + // ❌ Before + const { data } = Astro.props; + + // ✅ After + const { id } = Astro.params; + const entries = await getCollection('collection'); + const item = entries.find((e) => e.id === id); + ``` + +### Step 4: Update Data Fetching + +**Before:** + +```typescript +// Data passed via props from getStaticPaths +const { platforms, items } = Astro.props; +``` + +**After:** + +```typescript +// Fetch data directly +const allPlatforms = await getAllPlatforms(); +const items = await getItems(); +``` + +### Step 5: Handle Route Priority Issues + +If route priority doesn't work correctly: + +**Option A: Handle in Route File (Recommended)** + +- Check if route should be handled by another route in the file that matches first +- If so, render that route's content directly +- Handle trailing slash redirects **before** checking route validity +- Example: `[page].astro` detecting platform routes and rendering platform index + +```typescript +// In [page].astro - handles both pagination and platform routes +if (!/^\d+$/.test(page)) { + // Redirect trailing slash FIRST + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + // Then check if it's a platform + const isPlatform = await checkIfPlatform(page); + if (isPlatform) { + // Render platform index content directly + return renderPlatformIndex(page); + } +} +``` + +**Option B: Minimal Middleware (Only if absolutely necessary)** + +- Keep middleware simple - just pass through +- Avoid `context.rewrite()` as it can cause redirect loops +- Let route files handle their own logic + +### Step 6: Update Utility Functions + +Remove or modify utility functions that only generate static paths: + +**Before:** + +```typescript +// Only used for getStaticPaths +export async function generateStaticPaths() { + return paths.map(p => ({ params: p, props: {...} })); +} +``` + +**After:** + +```typescript +// Used for direct data fetching +export async function getAllItems() { + const entries = await getCollection('collection'); + return entries.map((entry) => ({ + id: entry.id, + data: entry.data, + // ... transform as needed + })); +} +``` + +### Step 7: Test All Routes + +Test every route type: + +```bash +# Test main index +curl http://localhost:4321/freedevtools/section/ + +# Test pagination +curl http://localhost:4321/freedevtools/section/2/ + +# Test category index +curl http://localhost:4321/freedevtools/section/category/ + +# Test category pagination +curl http://localhost:4321/freedevtools/section/category/2/ + +# Test item pages +curl http://localhost:4321/freedevtools/section/category/item/ +``` + +--- + +## Common Issues and Solutions + +### Issue 1: Route Collision Warnings + +**Error:** + +``` +[WARN] [router] The route "/section/[id]" is defined in both +"src/pages/section/[id].astro" and "src/pages/section/[slug].astro" +using SSR mode. A dynamic SSR route cannot be defined more than once. +``` + +**Solution:** + +- Consolidate into single route file +- Use logic to distinguish between different types +- Example: Check if param is numeric vs string + +### Issue 2: `getStaticPaths()` Ignored Warning + +**Error:** + +``` +[WARN] [router] getStaticPaths() ignored in dynamic page +/src/pages/section/[page].astro. Add `export const prerender = true;` +to prerender the page as static HTML during the build process. +``` + +**Solution:** + +- Remove `getStaticPaths()` function +- Add `export const prerender = false;` +- Fetch data directly in component + +### Issue 3: `Astro.props` is Undefined + +**Error:** + +``` +TypeError: Cannot read properties of undefined (reading 'length') +``` + +**Solution:** + +- `Astro.props` only works with `getStaticPaths()` in static mode +- In SSR, fetch data directly: + + ```typescript + // ❌ Won't work in SSR + const { items } = Astro.props; + + // ✅ Works in SSR + const items = await getItems(); + ``` + +### Issue 4: Redirect Loops + +**Error:** + +``` +ERR_TOO_MANY_REDIRECTS +HTTP 508: Astro detected a loop where you tried to call the rewriting logic more than four times +``` + +**Solution:** + +The most common cause is middleware trying to rewrite routes while the route file also handles redirects. **Fix: Remove middleware rewrites and handle redirects directly in the route file.** + +1. **Simplify or remove middleware:** + + ```typescript + // src/middleware.ts - Keep it simple + export const onRequest: MiddlewareHandler = async (context, next) => { + return next(); // Just pass through + }; + ``` + +2. **Handle trailing slashes in route file BEFORE other logic:** + + ```typescript + // In [page].astro or similar route file + const { page } = Astro.params; + const urlPath = Astro.url.pathname; + + // Check if page param is numeric + if (!/^\d+$/.test(page)) { + // Redirect to add trailing slash FIRST (before platform detection) + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + // Then check if it's a platform + const allPlatforms = await getAllPlatforms(); + const isPlatform = allPlatforms.some((p) => p.name === page); + // ... rest of logic + } + ``` + +3. **Key principles:** + - Handle trailing slash redirects **before** checking if route is valid + - Don't use `context.rewrite()` in middleware if route files also handle redirects + - One redirect per request - avoid multiple redirects in the same flow + +### Issue 5: Route Priority Not Working + +**Symptom:** + +- Wrong route handles a URL +- Expected: `/section/[category]/index.astro` +- Actual: `/section/[page].astro` matches first +- OR: Expected: `/section/[page].astro` +- Actual: `/section/[category]/index.astro` matches first (more specific route) + +**Solution:** + +**Option A: Handle in the route file that matches first (Recommended)** + +When a more specific (nested) route matches first, handle the conflicting case directly: + +```typescript +// In [category]/index.astro (matches first for /section/category/) +if (/^\d+$/.test(category)) { + // This is actually a pagination route - render it here + const currentPage = parseInt(category, 10); + // ... fetch and render pagination content directly + return ...; +} +// Otherwise handle as category index +``` + +**Option B: Handle in the less specific route file** + +When a less specific route matches first, detect and handle the more specific case: + +```typescript +// In [page].astro (matches first for /section/page/) +if (!/^\d+$/.test(page)) { + // This might be a category - check and handle + const allCategories = await getAllCategories(); + if (allCategories.includes(page)) { + // Render category index content directly + return ; + } +} +// Otherwise handle as pagination +``` + +**Key Principle:** Handle the conflicting case in whichever route file matches first, rather than trying to redirect or rewrite. + +--- + +## TLDR-Specific Implementation Details + +### Route Structure + +``` +src/pages/tldr/ +├── index.astro # Main index (/tldr/) +├── [page].astro # Main pagination (/tldr/2/) +│ └── Handles platform routes too (workaround for route priority) +├── [platform]/ +│ ├── index.astro # Platform index (/tldr/adb/) +│ └── [slug].astro # Platform pagination + commands +│ └── Handles both /tldr/adb/2/ and /tldr/adb/command/ +└── credits.astro # Static page +``` + +### Key Files Modified + +1. **`src/pages/tldr/[platform]/[slug].astro`** + - Consolidated `[command].astro` and `[page].astro` + - Checks if slug is numeric (pagination) or string (command) + - Fetches data directly using `getCollection('tldr')` + +2. **`src/pages/tldr/[page].astro`** + - Removed `getStaticPaths()` + - Fetches platforms directly using `getAllTldrPlatforms()` + - Handles platform index routes as workaround for route priority + - **Handles trailing slash redirects BEFORE platform detection** (prevents redirect loops) + - Early return for missing page param + +3. **`src/pages/tldr/[platform]/index.astro`** + - Removed `getStaticPaths()` + - Fetches commands directly using `getTldrPlatformCommands()` + +4. **`src/lib/tldr-utils.ts`** + - Kept utility functions but they now return data directly + - Removed static path generation functions (or made them SSR-compatible) + +5. **`src/middleware.ts`** + - Simplified to just pass through (no rewrites) + - Route files handle their own logic to avoid redirect loops + - Middleware rewrites were removed to prevent conflicts + +### Content Collection Usage + +```typescript +// Get all tldr entries +const tldrEntries = await getCollection('tldr'); + +// Access validated data (from content.config.ts schema) +entry.data.title; // string (required) +entry.data.description; // string (required) +entry.data.keywords; // string[] (optional) +entry.data.relatedTools; // array (optional) + +// Render markdown content +const { Content } = await render(entry); +``` + +--- + +## Checklist for Converting Other Sections + +When converting MCP, SVG icons, or other sections: + +- [ ] Identify all dynamic route files +- [ ] Check for route collisions (same URL pattern) +- [ ] Consolidate conflicting routes into single files +- [ ] Remove all `getStaticPaths()` functions +- [ ] Add `export const prerender = false;` to all SSR pages +- [ ] Replace `Astro.props` with direct data fetching +- [ ] Update utility functions to return data instead of static paths +- [ ] Test all route types (index, pagination, category, items) +- [ ] Handle route priority in route files (avoid middleware rewrites) +- [ ] Handle trailing slash redirects BEFORE checking route validity +- [ ] Verify content collection access works correctly +- [ ] Check for any build warnings about ignored `getStaticPaths()` +- [ ] Test redirects don't cause loops +- [ ] Verify all pages render correctly in SSR mode + +--- + +## MCP-Specific Implementation Details + +### Route Structure + +``` +src/pages/mcp/ +├── index.astro # Main index (/mcp/) +├── [page].astro # Main pagination (/mcp/2/) +│ └── Handles category detection (redirects to /category/1/) +├── [category]/ +│ ├── index.astro # Category index (/mcp/category/) +│ │ └── Handles numeric categories (pagination) directly +│ └── [slug].astro # Category pagination + repositories +│ └── Handles both /mcp/category/1/ and /mcp/category/repo-name/ +└── credits.astro # Static page +``` + +### Key Files Modified + +1. **`src/pages/mcp/[category]/[slug].astro`** + - Consolidated `[page].astro` and `[repositoryId].astro` + - Checks if slug is numeric (pagination) or string (repository) + - Fetches data directly using `getEntry('mcpCategoryData', category)` + - Uses `Object.entries()` to map repositories with IDs + +2. **`src/pages/mcp/[page].astro`** + - Removed `getStaticPaths()` + - Fetches categories directly using `getAllMcpCategories()` + - Handles category detection (redirects to `/category/1/` if it's a category name) + - Handles trailing slash redirects BEFORE checking category validity + +3. **`src/pages/mcp/[category]/index.astro`** + - Removed `getStaticPaths()` + - **Handles numeric categories directly** - renders pagination content when category is numeric + - This is a workaround for route priority: `[category]/index.astro` matches `/mcp/1/` before `[page].astro` + - Validates category exists before redirecting to page 1 + +4. **`src/lib/mcp-utils.ts`** + - Added SSR utility functions: + - `getAllMcpCategories()` - Get all categories for directory pagination + - `getAllMcpCategoryIds()` - Get category IDs for validation + - `getMcpCategoryById()` - Get category by ID + - `getMcpCategoryRepositories()` - Get repositories for a category + - `getMcpMetadata()` - Get MCP metadata + +### Route Priority Solution for MCP + +**Problem:** `/mcp/[category]` route collision between `[category]/index.astro` and `[page].astro` + +**Solution:** Handle numeric categories directly in `[category]/index.astro`: + +```typescript +// src/pages/mcp/[category]/index.astro +--- +const { category } = Astro.params; + +// If category is numeric, this is actually a pagination route +// Render pagination content directly (workaround for route priority) +if (/^\d+$/.test(category)) { + const currentPage = parseInt(category, 10); + // ... fetch and render pagination content + return ...; +} + +// Otherwise, validate category and redirect to page 1 +const allCategoryIds = await getAllMcpCategoryIds(); +if (!allCategoryIds.includes(category)) { + return new Response(null, { status: 404 }); +} +return Astro.redirect(`/freedevtools/mcp/${category}/1/`, 301); +--- +``` + +**Key Learning:** When a more specific (nested) route matches first, handle the conflicting case directly in that route file rather than trying to redirect or rewrite. + +### Content Collection Usage + +```typescript +// Get category entry directly (more efficient than getCollection + find) +const categoryEntry = await getEntry('mcpCategoryData', category); + +// Access category data +const categoryData = categoryEntry.data; +const repositories = categoryData.repositories; + +// Map repositories with IDs +const allRepositories = Object.entries(repositories).map( + ([repositoryId, server]) => ({ + ...server, + repositoryId: repositoryId, + }) +); +``` + +--- + +## Example: Converting MCP Pages (Actual Implementation) + +### Step 1: Identify Collisions + +- `/mcp/[category]` collision: `[category]/index.astro` vs `[page].astro` +- `/mcp/[category]/[page]` collision: `[page].astro` vs `[repositoryId].astro` + +### Step 2: Consolidate Routes + +**Consolidate `[page].astro` and `[repositoryId].astro` into `[slug].astro`:** + +```typescript +// [slug].astro - handles both pagination and repositories +const isNumericPage = /^\d+$/.test(slug); +const pageNumber = isNumericPage ? parseInt(slug, 10) : null; +const repositoryId = !isNumericPage ? slug : null; + +if (pageNumber !== null) { + // Handle pagination route +} else if (repositoryId) { + // Handle repository route +} +``` + +**Handle route priority in `[category]/index.astro`:** + +```typescript +// [category]/index.astro - handles numeric categories directly +if (/^\d+$/.test(category)) { + // Render pagination content directly + // This prevents [page].astro from needing to handle it +} +``` + +### Step 3: Remove Static Code + +- Remove all `getStaticPaths()` functions +- Add `export const prerender = false;` to all files +- Replace `Astro.props` with direct data fetching + +### Step 4: Test + +```bash +# Test main pagination +curl http://localhost:4321/freedevtools/mcp/1/ +curl http://localhost:4321/freedevtools/mcp/2/ + +# Test category index +curl http://localhost:4321/freedevtools/mcp/apis-and-http-requests/ + +# Test category pagination +curl http://localhost:4321/freedevtools/mcp/apis-and-http-requests/1/ +curl http://localhost:4321/freedevtools/mcp/apis-and-http-requests/2/ + +# Test repository pages +curl http://localhost:4321/freedevtools/mcp/scheduling-and-calendars/mumunha--cal_dot_com_mcpserver/ +``` + +--- + +## Performance Considerations + +### Caching in Middleware + +Middleware runs on every request, so cache expensive operations: + +```typescript +let cache: string[] | null = null; + +async function getExpensiveData() { + if (cache) return cache; + // Expensive operation + cache = await expensiveOperation(); + return cache; +} +``` + +### Content Collection Caching + +Astro automatically caches content collections, but be mindful of: + +- Large collections (consider pagination) +- Complex filtering operations +- Multiple `getCollection()` calls (cache results when possible) + +--- + +## Summary + +**Key Takeaways:** + +1. SSR mode requires direct data fetching, not `getStaticPaths()` +2. Route collisions must be resolved by consolidating routes +3. **Handle route priority in route files, not middleware** (avoids redirect loops) +4. **Handle conflicting cases in whichever route matches first** (more specific routes match before less specific ones) +5. **Handle trailing slash redirects BEFORE checking route validity** +6. **Use `getEntry()` for direct lookups** instead of `getCollection()` + `find()` when you know the ID +7. Content collections work the same in both modes +8. Always test all route types after conversion +9. Keep middleware simple - avoid rewrites that conflict with route handling + +**Files Typically Modified:** + +- Route files: Remove `getStaticPaths()`, add `prerender = false`, handle route priority directly +- Utility files: Change from path generation to data fetching +- Middleware: Keep simple (pass through) or remove entirely +- No changes needed to `content.config.ts` (collections work in both modes) + +**Critical Pattern for Redirect Loops:** + +Always handle trailing slash redirects **before** checking if a route is valid: + +```typescript +// ✅ CORRECT: Redirect first, then check +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} +const isValid = await checkRoute(page); + +// ❌ WRONG: Check first, then redirect (can cause loops) +const isValid = await checkRoute(page); +if (isValid && !urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} +``` diff --git a/frontend/scripts/ssr/convert_static_emojis_to_ssr.txt b/frontend/scripts/ssr/convert_static_emojis_to_ssr.txt new file mode 100644 index 0000000000..01253180ab --- /dev/null +++ b/frontend/scripts/ssr/convert_static_emojis_to_ssr.txt @@ -0,0 +1,65 @@ + +I want the ssr mode to work for my whole site, there is a lot of problems occuring one after another First lets focus on /emojis first + +When doing npm run build getting this error +19:00:03 [WARN] [router] The route "/emojis/apple-emojis/[category]" is defined in both "src/pages/emojis/apple-emojis/[category].astro" and "src/pages/emojis/apple-emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:00:03 [WARN] [router] The route "/emojis/discord-emojis/[category]" is defined in both "src/pages/emojis/discord-emojis/[category].astro" and "src/pages/emojis/discord-emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:00:03 [WARN] [router] The route "/emojis/[category]" is defined in both "src/pages/emojis/[category].astro" and "src/pages/emojis/[page].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:00:03 [WARN] [router] The route "/emojis/[category]" is defined in both "src/pages/emojis/[category].astro" and "src/pages/emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:00:03 [WARN] [router] The route "/emojis/[page]" is defined in both "src/pages/emojis/[page].astro" and "src/pages/emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. + +19:00:33 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/[page].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:39 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/[slug].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:39 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/[category].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/apple-emojis/[category].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/apple-emojis/[slug].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/discord-emojis/[category].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/discord-emojis/[slug].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/[category]/[page].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. + +18:44:18 [@astrojs/node] Server listening on http://localhost:4321 + +URL is slightly differnt in this emoji. +https://hexmos.com/freedevtools/emojis/activities/ is a category +Inside activiteis https://hexmos.com/freedevtools/emojis/jack-o-lantern/ is there +but my colleague has made mistkae of not including the cateogy in the URL + +All 12k pages of emoji are already insdexd by google, so we need to retain the urls as is and still get it working + +Many of the emoji pages are not working + +Working +http://localhost:4321/freedevtools/emojis/ +http://localhost:4321/freedevtools/emojis/activities/ +http://localhost:4321/freedevtools/emojis/activities/2/#pagination-info + + +Broken pages +http://localhost:4321/freedevtools/emojis/jack-o-lantern/ this is supposed to be the end page but it is shwoing pagination component in this which is wrong + +Not sure why + + +DO NOT MAKE THIS TRUE, I need SSR only +export const prerender = false; + +I dont want static + +Read the astro documentation i have given astro mcp aswell @mcp.json + +NPM run preview +19:07:59 [WARN] [router] The route "/emojis/apple-emojis/[category]" is defined in both "src/pages/emojis/apple-emojis/[category].astro" and "src/pages/emojis/apple-emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:07:59 [WARN] [router] The route "/emojis/discord-emojis/[category]" is defined in both "src/pages/emojis/discord-emojis/[category].astro" and "src/pages/emojis/discord-emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:07:59 [WARN] [router] The route "/emojis/[category]" is defined in both "src/pages/emojis/[category].astro" and "src/pages/emojis/[page].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:07:59 [WARN] [router] The route "/emojis/[category]" is defined in both "src/pages/emojis/[category].astro" and "src/pages/emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:07:59 [WARN] [router] The route "/emojis/[page]" is defined in both "src/pages/emojis/[page].astro" and "src/pages/emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. diff --git a/frontend/scripts/svg_icons_to_base64/build_precomputed_tables.ts b/frontend/scripts/svg_icons_to_base64/build_precomputed_tables.ts new file mode 100644 index 0000000000..72eb15f542 --- /dev/null +++ b/frontend/scripts/svg_icons_to_base64/build_precomputed_tables.ts @@ -0,0 +1,347 @@ +#!/usr/bin/env node +/** + * Build Precomputed Materialized Tables for SVG Icons Database + * + * This script: + * 1. Reads cluster and icon data from existing tables + * 2. Precomputes preview icons for each cluster + * 3. Creates optimized cluster_preview_precomputed table with precomputed preview_icons_json + * 4. Inserts precomputed data + * 5. Creates indexes for fast queries + * + * Run this during icon ingestion to regenerate materialized tables. + */ + +import path from 'path'; +import sqlite3 from 'sqlite3'; +import type { Cluster, Icon, RawClusterRow, RawIconRow } from '../../db/svg_icons/svg-icons-schema'; + +const DB_PATH = path.resolve(process.cwd(), 'db/all_dbs/svg-icons-db-v1.db'); + +interface PreviewIcon { + id: number; + name: string; + base64: string; + img_alt: string; +} + +// Helper to promisify sqlite3 operations +function promisifyRun(db: sqlite3.Database, sql: string, params: any[] = []): Promise { + return new Promise((resolve, reject) => { + db.run(sql, params, (err) => { + if (err) reject(err); + else resolve(); + }); + }); +} + +function promisifyAll(db: sqlite3.Database, sql: string, params: any[] = []): Promise { + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) reject(err); + else resolve((rows || []) as T[]); + }); + }); +} + +function promisifyGet(db: sqlite3.Database, sql: string, params: any[] = []): Promise { + return new Promise((resolve, reject) => { + db.get(sql, params, (err, row) => { + if (err) reject(err); + else resolve(row as T | undefined); + }); + }); +} + +// Open database connection +function openDb(): Promise { + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READWRITE, (err) => { + if (err) { + reject(err); + return; + } + resolve(db); + }); + }); +} + +// Step 1: Read cluster and icon data into memory +async function readAllData(db: sqlite3.Database): Promise<{ + clusters: Cluster[]; + icons: Icon[]; +}> { + console.log('[BUILD_PRECOMPUTED] Reading cluster and icon data from existing tables...'); + const startTime = Date.now(); + + // Read clusters + const clusterRows = await promisifyAll( + db, + `SELECT id, name, count, source_folder, path, + json(keywords) as keywords, json(tags) as tags, + title, description, practical_application, json(alternative_terms) as alternative_terms, + about, json(why_choose_us) as why_choose_us + FROM cluster ORDER BY name` + ); + + const clusters: Cluster[] = clusterRows.map((row) => ({ + ...row, + keywords: JSON.parse(row.keywords || '[]') as string[], + tags: JSON.parse(row.tags || '[]') as string[], + alternative_terms: JSON.parse(row.alternative_terms || '[]') as string[], + why_choose_us: JSON.parse(row.why_choose_us || '[]') as string[], + })); + + // Read icons (needed for precomputing preview icons) + const iconRows = await promisifyAll( + db, + `SELECT id, cluster, name, base64, description, usecases, + json(synonyms) as synonyms, json(tags) as tags, + industry, emotional_cues, enhanced, img_alt + FROM icon ORDER BY name` + ); + + const icons: Icon[] = iconRows.map((row) => ({ + ...row, + title: null, // Not in database table, but in TypeScript schema + synonyms: JSON.parse(row.synonyms || '[]') as string[], + tags: JSON.parse(row.tags || '[]') as string[], + })); + + const elapsed = Date.now() - startTime; + console.log(`[BUILD_PRECOMPUTED] Read ${clusters.length} clusters, ${icons.length} icons in ${elapsed}ms`); + + return { + clusters, + icons, + }; +} + +// Step 2: Precompute preview icons for each cluster +function precomputePreviewIcons( + clusters: Cluster[], + icons: Icon[], + previewIconsPerCluster: number = 6 +): Map { + console.log(`[BUILD_PRECOMPUTED] Precomputing preview icons (${previewIconsPerCluster} per cluster)...`); + const startTime = Date.now(); + + // Group icons by cluster + const iconsByCluster = new Map(); + for (const icon of icons) { + if (!iconsByCluster.has(icon.cluster)) { + iconsByCluster.set(icon.cluster, []); + } + iconsByCluster.get(icon.cluster)!.push(icon); + } + + // Sort icons by name within each cluster + for (const [cluster, clusterIcons] of iconsByCluster.entries()) { + clusterIcons.sort((a, b) => a.name.localeCompare(b.name)); + } + + // Build preview icons for each cluster + const previewIconsMap = new Map(); + + for (const cluster of clusters) { + // Try both source_folder and name as cluster keys + const clusterKey = cluster.source_folder || cluster.name; + const clusterIcons = iconsByCluster.get(clusterKey) || []; + + const previewIcons: PreviewIcon[] = clusterIcons + .slice(0, previewIconsPerCluster) + .map((icon) => ({ + id: icon.id, + name: icon.name, + base64: icon.base64, + img_alt: icon.img_alt, + })); + + previewIconsMap.set(cluster.name, previewIcons); + } + + const elapsed = Date.now() - startTime; + console.log(`[BUILD_PRECOMPUTED] Precomputed preview icons in ${elapsed}ms`); + + return previewIconsMap; +} + +// Step 3: Create materialized table +async function createMaterializedTables(db: sqlite3.Database): Promise { + console.log('[BUILD_PRECOMPUTED] Creating materialized table...'); + + // Drop existing table if it exists + await promisifyRun(db, 'DROP TABLE IF EXISTS cluster_preview_precomputed;'); + + // Create cluster_preview_precomputed table with precomputed preview_icons_json + await promisifyRun(db, ` + CREATE TABLE cluster_preview_precomputed ( + id INTEGER PRIMARY KEY, + name TEXT, + source_folder TEXT, + path TEXT, + count INTEGER, + keywords_json TEXT, + tags_json TEXT, + title TEXT, + description TEXT, + practical_application TEXT, + alternative_terms_json TEXT, + about TEXT, + why_choose_us_json TEXT, + preview_icons_json TEXT + ); + `); + + console.log('[BUILD_PRECOMPUTED] Materialized table created'); +} + +// Step 4: Insert precomputed data +async function insertPrecomputedData( + db: sqlite3.Database, + clusters: Cluster[], + previewIconsMap: Map +): Promise { + console.log('[BUILD_PRECOMPUTED] Inserting precomputed data...'); + const startTime = Date.now(); + + const insertClusterSql = ` + INSERT INTO cluster_preview_precomputed ( + id, name, source_folder, path, count, + keywords_json, tags_json, title, description, + practical_application, alternative_terms_json, + about, why_choose_us_json, preview_icons_json + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + + // Insert clusters with precomputed preview icons + for (const cluster of clusters) { + const previewIcons = previewIconsMap.get(cluster.name) || []; + const previewIconsJson = JSON.stringify(previewIcons); + + await promisifyRun(db, insertClusterSql, [ + cluster.id, + cluster.name, + cluster.source_folder, + cluster.path, + cluster.count, + JSON.stringify(cluster.keywords), + JSON.stringify(cluster.tags), + cluster.title, + cluster.description, + cluster.practical_application, + JSON.stringify(cluster.alternative_terms), + cluster.about, + JSON.stringify(cluster.why_choose_us), + previewIconsJson, + ]); + } + + // Commit transaction + await promisifyRun(db, 'COMMIT'); + + const elapsed = Date.now() - startTime; + console.log(`[BUILD_PRECOMPUTED] Inserted ${clusters.length} clusters in ${elapsed}ms`); +} + +// Step 5: Create indexes +async function createIndexes(db: sqlite3.Database): Promise { + console.log('[BUILD_PRECOMPUTED] Creating indexes...'); + + // Drop old indexes if they exist + await promisifyRun(db, 'DROP INDEX IF EXISTS idx_cluster_preview_precomputed_name;'); + await promisifyRun(db, 'DROP INDEX IF EXISTS idx_cluster_name;'); + await promisifyRun(db, 'DROP INDEX IF EXISTS idx_icon_cluster_name;'); + + // Create cluster index + await promisifyRun(db, ` + CREATE INDEX IF NOT EXISTS idx_cluster_name + ON cluster(name); + `); + + // Create icon index + await promisifyRun(db, ` + CREATE INDEX IF NOT EXISTS idx_icon_cluster_name + ON icon(cluster, name); + `); + + // Create cluster_preview_precomputed index + await promisifyRun(db, ` + CREATE INDEX IF NOT EXISTS idx_cluster_preview_name + ON cluster_preview_precomputed(name); + `); + + console.log('[BUILD_PRECOMPUTED] Indexes created'); +} + +// Step 6: Verify the materialized table +async function verifyTables(db: sqlite3.Database): Promise { + console.log('[BUILD_PRECOMPUTED] Verifying materialized table...'); + + const clusterCount = await promisifyGet<{ count: number }>( + db, + 'SELECT COUNT(*) as count FROM cluster_preview_precomputed' + ); + console.log(`[BUILD_PRECOMPUTED] cluster_preview_precomputed: ${clusterCount?.count || 0} rows`); + + // Sample preview icons + const sampleCluster = await promisifyGet<{ name: string; preview_icons_json: string }>( + db, + 'SELECT name, preview_icons_json FROM cluster_preview_precomputed LIMIT 1' + ); + if (sampleCluster) { + const previewIcons = JSON.parse(sampleCluster.preview_icons_json || '[]'); + console.log(`[BUILD_PRECOMPUTED] Sample cluster "${sampleCluster.name}" has ${previewIcons.length} preview icons`); + } +} + +// Main function +async function main(): Promise { + console.log(`[BUILD_PRECOMPUTED] Starting build of precomputed materialized tables...`); + console.log(`[BUILD_PRECOMPUTED] Database: ${DB_PATH}`); + + const db = await openDb(); + + try { + // Begin transaction for better performance + await promisifyRun(db, 'BEGIN TRANSACTION'); + + // Step 1: Read cluster and icon data + const { clusters, icons } = await readAllData(db); + + // Step 2: Precompute preview icons + const previewIconsMap = precomputePreviewIcons(clusters, icons, 6); + + // Step 3: Create materialized table + await createMaterializedTables(db); + + // Step 4: Insert precomputed data + await insertPrecomputedData(db, clusters, previewIconsMap); + + // Step 5: Create indexes + await createIndexes(db); + + // Step 6: Verify + await verifyTables(db); + + console.log('[BUILD_PRECOMPUTED] ✅ Successfully built precomputed materialized tables!'); + } catch (error) { + console.error('[BUILD_PRECOMPUTED] ❌ Error building precomputed tables:', error); + await promisifyRun(db, 'ROLLBACK'); + throw error; + } finally { + db.close((err) => { + if (err) { + console.error('[BUILD_PRECOMPUTED] Error closing database:', err); + } + }); + } +} + +// Run the script +main().catch((error) => { + console.error(error); + process.exit(1); +}); + diff --git a/frontend/scripts/svg_icons_to_base64/build_sqlite_from_json.py b/frontend/scripts/svg_icons_to_base64/build_sqlite_from_json.py index cda1ebc334..25a758793d 100644 --- a/frontend/scripts/svg_icons_to_base64/build_sqlite_from_json.py +++ b/frontend/scripts/svg_icons_to_base64/build_sqlite_from_json.py @@ -4,7 +4,7 @@ - Scans scripts/svg_icons_to_base64/base64_svg_icons/*.json - Reads data/cluster_svg.json for metadata -- Creates SQLite DB at db/all_dbs/svg-icons-db.db +- Creates SQLite DB at db/all_dbs/svg-icons-db-v1.db - Table: icon(id INTEGER PRIMARY KEY AUTOINCREMENT, cluster TEXT, name TEXT, base64 TEXT, ...) - Table: cluster(name TEXT PRIMARY KEY, count INTEGER, source_folder TEXT, ...) - Table: overview(id INTEGER PRIMARY KEY CHECK(id = 1), total_count INTEGER) @@ -19,7 +19,7 @@ BASE_DIR = Path(__file__).parent JSON_DIR = BASE_DIR / "base64_svg_icons" CLUSTER_SVG_PATH = BASE_DIR.parent.parent / "data" / "cluster_svg.json" -DB_PATH = Path(__file__).parent.parent.parent / "db" / "all_dbs" / "svg-icons-db.db" +DB_PATH = Path(__file__).parent.parent.parent / "db" / "all_dbs" / "svg-icons-db-v1.db" def ensure_schema(conn: sqlite3.Connection) -> None: @@ -27,18 +27,22 @@ def ensure_schema(conn: sqlite3.Connection) -> None: cur.execute( """ CREATE TABLE IF NOT EXISTS icon ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id INTEGER, + url_hash INTEGER PRIMARY KEY, cluster TEXT NOT NULL, name TEXT NOT NULL, base64 TEXT NOT NULL, description TEXT DEFAULT '', - usecases TEXT DEFAULT '', + usecases TEXT DEFAULT '[]', synonyms TEXT DEFAULT '[]', tags TEXT DEFAULT '[]', industry TEXT DEFAULT '', emotional_cues TEXT DEFAULT '', - enhanced INTEGER DEFAULT 0 - ); + enhanced INTEGER DEFAULT 0, + img_alt TEXT DEFAULT '', + ai_image_alt_generated INTEGER DEFAULT 0, + url TEXT NOT NULL + ) WITHOUT ROWID; """ ) cur.execute("CREATE INDEX IF NOT EXISTS idx_icon_cluster ON icon(cluster);") @@ -66,11 +70,17 @@ def ensure_schema(conn: sqlite3.Connection) -> None: cur.execute( """ CREATE TABLE IF NOT EXISTS overview ( - id INTEGER PRIMARY KEY CHECK(id = 1), - total_count INTEGER NOT NULL + id INTEGER PRIMARY KEY, + total_count INTEGER NOT NULL, + name TEXT ); """ ) + try: + cur.execute("ALTER TABLE overview ADD COLUMN name TEXT;") + except sqlite3.OperationalError: + # Column already exists + pass conn.commit() @@ -210,8 +220,19 @@ def populate_cluster_and_overview(conn: sqlite3.Connection) -> None: cur.execute("SELECT COUNT(*) FROM icon;") total_count = cur.fetchone()[0] + # Compute cluster count for overview + cur.execute("SELECT COUNT(*) FROM cluster;") + cluster_count = cur.fetchone()[0] + cur.execute("DELETE FROM overview;") - cur.execute("INSERT INTO overview (id, total_count) VALUES (1, ?);", (total_count,)) + cur.execute( + "INSERT INTO overview (id, total_count, name) VALUES (1, ?, 'icons');", + (total_count,), + ) + cur.execute( + "INSERT INTO overview (id, total_count, name) VALUES (2, ?, 'cluster');", + (cluster_count,), + ) conn.commit() print(f"✓ Populated cluster and overview tables") diff --git a/frontend/scripts/tldr/Makefile b/frontend/scripts/tldr/Makefile new file mode 100644 index 0000000000..e9080ca303 --- /dev/null +++ b/frontend/scripts/tldr/Makefile @@ -0,0 +1,21 @@ +.PHONY: build run clean + +BINARY_NAME=tldr_to_db +SQLITE_DB_FILE=/home/gk/hexmos/FreeDevTools/frontend/db/all_dbs/tldr-db-v1.db +SQLITE_QUERY_FILE=query.sql + +SQL_BINARY_NAME=sqlite-lookup + +build: + go build -o $(BINARY_NAME) tldr_to_db.go + +run: + go run tldr_to_db.go + +clean: + rm -f $(BINARY_NAME) + + + +execute: + ./$(SQL_BINARY_NAME) --show-data $(SQLITE_QUERY_FILE) $(SQLITE_DB_FILE) \ No newline at end of file diff --git a/frontend/scripts/tldr/README.md b/frontend/scripts/tldr/README.md new file mode 100644 index 0000000000..d1a40e657a --- /dev/null +++ b/frontend/scripts/tldr/README.md @@ -0,0 +1,99 @@ +# TLDR to DB Converter + +This Go script parses TLDR pages (markdown files) and populates a SQLite database. + +## Prerequisites + +- Go installed (1.18+) +- SQLite3 + +## Directory Structure + +The script expects the following directory structure relative to its location: + +- Input Data: `../../data/tldr` (Contains the TLDR markdown files) +- Output Database: `../../db/all_dbs/tldr-db-v1.db` + +## Setup + +1. Navigate to the script directory: + + ```bash + cd scripts/tldr + ``` + +2. Install dependencies: + + ```bash + go mod tidy + ``` + +## Usage + +Run the script using: + +```bash +go run tldr_to_db.go +``` + +The script will: + +1. Create/Reset the database at `../../db/all_dbs/tldr-db.db`. +2. Walk through the `../../data/tldr` directory. +3. Parse each markdown file (frontmatter, content, examples). +4. Insert the data into the database. +5. Populate cluster and overview tables. + +## Output + +The script outputs progress to the console and prints the total time taken upon completion. + +## How it Works + +The `tldr_to_db.go` script performs the following steps to transform raw markdown files into a structured SQLite database: + +### 1. Database Initialization + +- Creates the database file at `../../db/all_dbs/tldr-db-v1.db`. +- Defines the schema with three main tables: + - **`pages`**: Stores individual command pages. + - `url_hash` (PK): Integer hash of the URL. + - `url`: The full path (e.g., `/freedevtools/tldr/common/tar/`). + - `cluster_hash`: Integer hash of the cluster name (FK). + - `title`: Title of the page. + - `description`: Description of the page. + - `html_content`: Rendered HTML from markdown. + - `metadata`: JSON string containing keywords and features. + - **`cluster`**: Stores metadata for command groups (platforms like `common`, `linux`). + - `hash` (PK): Integer hash of the cluster name. + - `name`: Cluster name (e.g., `common`). + - `count`: Total number of commands in the cluster. + - `preview_commands_json`: JSON array of the first 5 commands for preview. + - **`overview`**: Stores global statistics. + - `total_count`: Total number of commands across all platforms. + - `total_clusters`: Total number of clusters. + - `total_pages`: Total number of pages. + +### 2. File Parsing + +- Walks through the `../../data/tldr` directory. +- Parses each `.md` file: + - Extracts **Frontmatter** (YAML) for metadata like title and description. + - Renders **Markdown Body** to HTML using `gomarkdown`. + - Generates a unique **URL Hash** based on the category and filename. + +### 3. Data Insertion + +- **Pages**: Inserts every parsed page into the `pages` table. +- **Clusters**: + - Groups pages by their platform (cluster). + - Sorts pages alphabetically within each cluster. + - Generates a preview JSON for the first 5 commands. + - Inserts the aggregated data into the `cluster` table. +- **Overview**: Inserts the total count of processed pages into the `overview` table. + +This approach ensures that the frontend can efficiently fetch: + +- Individual pages by URL hash. +- Cluster summaries (for lists and pagination) by cluster hash. +- Global stats from the overview table. diff --git a/frontend/scripts/tldr/go.mod b/frontend/scripts/tldr/go.mod new file mode 100644 index 0000000000..8b80334435 --- /dev/null +++ b/frontend/scripts/tldr/go.mod @@ -0,0 +1,9 @@ +module tldr_to_db + +go 1.24.4 + +require ( + github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a + github.com/mattn/go-sqlite3 v1.14.32 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/frontend/scripts/tldr/go.sum b/frontend/scripts/tldr/go.sum new file mode 100644 index 0000000000..072b002583 --- /dev/null +++ b/frontend/scripts/tldr/go.sum @@ -0,0 +1,8 @@ +github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= +github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/frontend/scripts/tldr/tldr_to_db.go b/frontend/scripts/tldr/tldr_to_db.go new file mode 100644 index 0000000000..46090d1d8b --- /dev/null +++ b/frontend/scripts/tldr/tldr_to_db.go @@ -0,0 +1,370 @@ +package main + +import ( + "crypto/sha256" + "database/sql" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + _ "github.com/mattn/go-sqlite3" + "gopkg.in/yaml.v3" +) + +// Paths +const ( + DataDir = "../../data/tldr" + DbPath = "../../db/all_dbs/tldr-db-v1.db" +) + +// Structs matching DB schema +type Page struct { + UrlHash int64 + Url string // Kept for reference + Title string + Description string + HtmlContent string + Metadata string // JSON (keywords, features) +} + +type MainPage struct { + Hash int64 + Data string // JSON + TotalCount int + Url string +} + +// Intermediate struct for processing +type ProcessedPage struct { + UrlHash int64 + Url string + Cluster string + Name string + Platform string + Title string + Description string + Keywords []string + Features []string + HtmlContent string + Path string +} + +type PageMetadata struct { + Keywords []string `json:"keywords"` + Features []string `json:"features"` +} + +type Frontmatter struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Category string `yaml:"category"` + Path string `yaml:"path"` + Keywords []string `yaml:"keywords"` + Features []string `yaml:"features"` +} + +// --- Hashing Functions --- + +func createFullHash(category, lastPath string) string { + category = strings.ToLower(strings.TrimSpace(category)) + lastPath = strings.ToLower(strings.TrimSpace(lastPath)) + uniqueStr := fmt.Sprintf("%s/%s", category, lastPath) + hash := sha256.Sum256([]byte(uniqueStr)) + return hex.EncodeToString(hash[:]) +} + +func get8Bytes(fullHash string) int64 { + hexPart := fullHash[:16] + bytesVal, err := hex.DecodeString(hexPart) + if err != nil { + log.Fatalf("Failed to decode hex: %v", err) + } + return int64(binary.BigEndian.Uint64(bytesVal)) +} + +func hashString(s string) string { + hash := sha256.Sum256([]byte(s)) + return hex.EncodeToString(hash[:]) +} + +// --- Markdown Parsing --- + +func parseTldrFile(path string) (*ProcessedPage, error) { + contentBytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + content := string(contentBytes) + + parts := regexp.MustCompile(`(?m)^---\s*$`).Split(content, 3) + if len(parts) < 3 { + if strings.HasPrefix(content, "---") { + parts = regexp.MustCompile(`(?m)^---\s*$`).Split(content, 3) + if len(parts) >= 3 && parts[0] == "" { + } else { + return nil, nil + } + } else { + return nil, nil + } + } + + frontmatterRaw := parts[1] + markdownBody := strings.TrimSpace(parts[2]) + + lines := strings.Split(markdownBody, "\n") + if len(lines) > 0 && strings.HasPrefix(strings.TrimSpace(lines[0]), "# ") { + markdownBody = strings.Join(lines[1:], "\n") + } + markdownBody = strings.TrimSpace(markdownBody) + + var fm Frontmatter + if err := yaml.Unmarshal([]byte(frontmatterRaw), &fm); err != nil { + log.Printf("Error parsing YAML in %s: %v", filepath.Base(path), err) + return nil, nil + } + + title := fm.Title + description := fm.Description + + extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock + p := parser.NewWithExtensions(extensions) + doc := p.Parse([]byte(markdownBody)) + + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + htmlBytes := markdown.Render(doc, renderer) + htmlContent := string(htmlBytes) + + cluster := filepath.Base(filepath.Dir(path)) + name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + fullHash := createFullHash(cluster, name) + urlHash := get8Bytes(fullHash) + pathUrl := fm.Path + if pathUrl == "" { + pathUrl = fmt.Sprintf("/freedevtools/tldr/%s/%s/", cluster, name) + } + if !strings.HasSuffix(pathUrl, "/") { + pathUrl += "/" + } + + return &ProcessedPage{ + UrlHash: urlHash, + Url: pathUrl, + Cluster: cluster, + Name: name, + Platform: fm.Category, + Title: title, + Description: description, + Keywords: fm.Keywords, + Features: fm.Features, + HtmlContent: htmlContent, + Path: pathUrl, + }, nil +} + +// --- Database --- + +func ensureSchema(db *sql.DB) error { + // Drop tables if they exist to force schema update + if _, err := db.Exec("DROP TABLE IF EXISTS pages"); err != nil { + return err + } + if _, err := db.Exec("DROP TABLE IF EXISTS cluster"); err != nil { + return err + } + if _, err := db.Exec("DROP TABLE IF EXISTS overview"); err != nil { + return err + } + + // Pages table - Simplified + fmt.Println("Creating pages table...") + _, err := db.Exec(` + CREATE TABLE pages ( + url_hash INTEGER PRIMARY KEY, + url TEXT NOT NULL, -- Kept for reference + cluster_hash INTEGER NOT NULL, + title TEXT DEFAULT '', + description TEXT DEFAULT '', + html_content TEXT DEFAULT '', + metadata TEXT DEFAULT '{}' -- JSON (keywords, features) + ) WITHOUT ROWID; + `) + if err != nil { + return fmt.Errorf("failed to create pages table: %w", err) + } + + // Cluster table - Cluster Metadata (similar to emojis category) + fmt.Println("Creating cluster table...") + _, err = db.Exec(` + CREATE TABLE cluster ( + hash INTEGER PRIMARY KEY, + name TEXT NOT NULL, + count INTEGER NOT NULL, + preview_commands_json TEXT DEFAULT '[]' -- JSON list of preview commands + ) WITHOUT ROWID; + `) + if err != nil { + return fmt.Errorf("failed to create cluster table: %w", err) + } + + // Overview table + fmt.Println("Creating overview table...") + _, err = db.Exec(` + CREATE TABLE overview ( + id INTEGER PRIMARY KEY CHECK(id = 1), + total_count INTEGER NOT NULL, + total_clusters INTEGER NOT NULL DEFAULT 0, + total_pages INTEGER NOT NULL DEFAULT 0 + ); + `) + if err != nil { + return fmt.Errorf("failed to create overview table: %w", err) + } + return nil +} + +func main() { + start := time.Now() + + if err := os.MkdirAll(filepath.Dir(DbPath), 0755); err != nil { + log.Fatal(err) + } + os.Remove(DbPath) + + db, err := sql.Open("sqlite3", DbPath) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + if err := ensureSchema(db); err != nil { + log.Fatal(err) + } + + // 1. Parse all files into memory + fmt.Printf("Scanning %s...\n", DataDir) + var allPages []*ProcessedPage + var allUrls []string + + err = filepath.WalkDir(DataDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && strings.HasSuffix(d.Name(), ".md") { + page, err := parseTldrFile(path) + if err != nil { + log.Printf("Error parsing %s: %v", path, err) + return nil + } + if page != nil { + allPages = append(allPages, page) + } + } + return nil + }) + if err != nil { + log.Fatal(err) + } + + // 2. Insert Pages + fmt.Println("Inserting pages...") + tx, err := db.Begin() + if err != nil { + log.Fatal(err) + } + stmt, err := tx.Prepare("INSERT INTO pages (url_hash, url, cluster_hash, title, description, html_content, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)") + if err != nil { + log.Fatal(err) + } + defer stmt.Close() + + for _, p := range allPages { + meta := PageMetadata{ + Keywords: p.Keywords, + Features: p.Features, + } + metaJson, _ := json.Marshal(meta) + + // Calculate cluster hash + clusterHash := get8Bytes(hashString(p.Cluster)) + + _, err = stmt.Exec(p.UrlHash, p.Url, clusterHash, p.Title, p.Description, p.HtmlContent, string(metaJson)) + if err != nil { + log.Printf("Error inserting page %s: %v", p.Name, err) + } + allUrls = append(allUrls, p.Url) + } + tx.Commit() + + // 3. Generate Cluster (Cluster Metadata) + fmt.Println("Generating cluster metadata...") + pagesByCluster := make(map[string][]*ProcessedPage) + for _, p := range allPages { + pagesByCluster[p.Cluster] = append(pagesByCluster[p.Cluster], p) + } + + tx, err = db.Begin() + if err != nil { + log.Fatal(err) + } + stmt, err = tx.Prepare("INSERT INTO cluster (hash, name, count, preview_commands_json) VALUES (?, ?, ?, ?)") + if err != nil { + log.Fatal(err) + } + + for cluster, pages := range pagesByCluster { + // Sort pages by name + sort.Slice(pages, func(i, j int) bool { + return pages[i].Name < pages[j].Name + }) + + totalCount := len(pages) + + // Get top 5 commands for preview + var commandPreviews []map[string]string + previewCount := 5 + if len(pages) < previewCount { + previewCount = len(pages) + } + for k := 0; k < previewCount; k++ { + commandPreviews = append(commandPreviews, map[string]string{ + "name": pages[k].Name, + "url": fmt.Sprintf("/freedevtools/tldr/%s/%s/", cluster, pages[k].Name), + }) + } + previewJson, _ := json.Marshal(commandPreviews) + + // Hash: cluster (e.g., common) + hash := get8Bytes(hashString(cluster)) + + _, err = stmt.Exec(hash, cluster, totalCount, string(previewJson)) + if err != nil { + log.Printf("Error inserting cluster %s: %v", cluster, err) + } + } + tx.Commit() + + // 4. Overview + totalClusters := len(pagesByCluster) + totalPages := len(allPages) + if _, err := db.Exec("INSERT INTO overview (id, total_count, total_clusters, total_pages) VALUES (1, ?, ?, ?)", totalPages, totalClusters, totalPages); err != nil { + log.Fatal(err) + } + + elapsed := time.Since(start) + fmt.Printf("Finished! Processed %d pages and %d URLs in %s.\n", len(allPages), len(allUrls), elapsed) +} diff --git a/frontend/scripts/tldr/tldr_to_db.py b/frontend/scripts/tldr/tldr_to_db.py new file mode 100644 index 0000000000..60ec9d16cd --- /dev/null +++ b/frontend/scripts/tldr/tldr_to_db.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +""" +Build a SQLite database from TLDR markdown files. + +- Scans data/tldr/*/*.md +- Creates SQLite DB at db/all_dbs/tldr-db.db +- Table: pages (WITHOUT ROWID) +- Table: cluster +- Table: overview +""" + +import hashlib +import json +import re +import sqlite3 +import struct +import yaml +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +BASE_DIR = Path(__file__).parent +DATA_DIR = BASE_DIR.parent.parent / "data" / "tldr" +DB_PATH = BASE_DIR.parent.parent / "db" / "all_dbs" / "tldr-db.db" + + +def create_full_hash(category: str, last_path: str) -> str: + """Create a SHA-256 hash from category and name.""" + # Normalize input: remove leading/trailing slashes, lowercase + category = category.strip().lower() + last_path = last_path.strip().lower() + + # Create unique string + unique_str = f"{category}/{last_path}" + + # Compute SHA-256 hash + return hashlib.sha256(unique_str.encode("utf-8")).hexdigest() + + +def get_8_bytes(full_hash: str) -> int: + """Get the first 8 bytes of the hash as a signed 64-bit integer.""" + # Take first 16 hex chars (8 bytes) + hex_part = full_hash[:16] + # Convert to bytes + bytes_val = bytes.fromhex(hex_part) + # Unpack as signed 64-bit integer (big-endian) + return struct.unpack(">q", bytes_val)[0] + + +def hash_name_to_key(name: str) -> str: + """Hash a name to a signed 64-bit integer string.""" + hash_bytes = hashlib.sha256(name.encode("utf-8")).digest() + # Take first 8 bytes and interpret as big-endian signed 64-bit integer + val = struct.unpack(">q", hash_bytes[:8])[0] + return str(val) + + +def ensure_schema(conn: sqlite3.Connection) -> None: + cur = conn.cursor() + + # Main table for TLDR pages using consistent hashing + cur.execute( + """ + CREATE TABLE IF NOT EXISTS pages ( + url_hash INTEGER PRIMARY KEY, + url TEXT NOT NULL, + cluster TEXT NOT NULL, -- Directory name (e.g., 'git', 'aws') + name TEXT NOT NULL, -- Command name (e.g., 'git-commit') + platform TEXT DEFAULT '', -- From frontmatter 'category' (e.g., 'common', 'linux') + title TEXT DEFAULT '', -- From frontmatter + description TEXT DEFAULT '', -- From frontmatter or markdown blockquote + more_info_url TEXT DEFAULT '', -- From markdown blockquote + keywords TEXT DEFAULT '[]', -- JSON array from frontmatter + features TEXT DEFAULT '[]', -- JSON array from frontmatter + examples TEXT DEFAULT '[]', -- JSON array of {description, cmd} + raw_content TEXT DEFAULT '', -- Full markdown content + path TEXT DEFAULT '' -- URL path from frontmatter + ) WITHOUT ROWID; + """ + ) + + # Index for efficient cluster grouping + cur.execute("CREATE INDEX IF NOT EXISTS idx_pages_cluster ON pages(cluster);") + + # Cluster table (categories) + cur.execute( + """ + CREATE TABLE IF NOT EXISTS cluster ( + name TEXT PRIMARY KEY, + hash_name TEXT NOT NULL, + count INTEGER NOT NULL, + description TEXT DEFAULT '' -- Kept empty as per previous implementation + ); + """ + ) + + # Index for hash_name lookup + cur.execute("CREATE INDEX IF NOT EXISTS idx_cluster_hash_name ON cluster(hash_name);") + + # Overview table + cur.execute( + """ + CREATE TABLE IF NOT EXISTS overview ( + id INTEGER PRIMARY KEY CHECK(id = 1), + total_count INTEGER NOT NULL + ); + """ + ) + + conn.commit() + + +def parse_tldr_file(file_path: Path) -> Optional[Dict[str, Any]]: + """ + Parse a TLDR markdown file. + Returns a dictionary with extracted data or None if parsing fails. + """ + try: + content = file_path.read_text(encoding="utf-8") + except Exception as e: + print(f"Error reading {file_path}: {e}") + return None + + # Split frontmatter and content + parts = re.split(r"^---\s*$", content, maxsplit=2, flags=re.MULTILINE) + if len(parts) < 3: + print(f"Skipping {file_path.name}: Invalid frontmatter format") + return None + + frontmatter_raw = parts[1] + markdown_body = parts[2] + + # Parse Frontmatter + try: + fm = yaml.safe_load(frontmatter_raw) or {} + except yaml.YAMLError as e: + print(f"Error parsing YAML in {file_path.name}: {e}") + return None + + # Extract fields from frontmatter + title = fm.get("title", "") + # Clean title: remove " | Online Free DevTools by Hexmos" suffix if present + title = title.split(" | ")[0] + + platform = fm.get("category", "") + path_url = fm.get("path", "") + keywords = json.dumps(fm.get("keywords", [])) + features = json.dumps(fm.get("features", [])) + + # Parse Markdown Body + description_lines = [] + more_info_url = "" + examples = [] + + lines = markdown_body.strip().splitlines() + i = 0 + while i < len(lines): + line = lines[i].strip() + + # Blockquotes (Description & More Info) + if line.startswith(">"): + clean_line = line.lstrip("> ").strip() + if clean_line.startswith("More information:"): + # Extract URL + match = re.search(r"<(.*?)>", clean_line) + if match: + more_info_url = match.group(1) + else: + description_lines.append(clean_line) + + # Examples + elif line.startswith("- "): + example_desc = line.lstrip("- ").strip() + # Look ahead for the command + cmd = "" + j = i + 1 + while j < len(lines): + next_line = lines[j].strip() + if next_line.startswith("`") and next_line.endswith("`"): + cmd = next_line.strip("`") + # Handle {{ }} placeholders if needed, but keeping them as is is usually fine for display + i = j # Advance main loop + break + elif next_line == "": + j += 1 + continue + else: + # Found something else, stop looking for command + break + + if cmd: + examples.append({"description": example_desc, "cmd": cmd}) + + i += 1 + + description = " ".join(description_lines) + + # Calculate hash + # Use directory name as cluster (e.g., 'adb', 'common') + # This ensures consistent grouping by physical location + cluster = file_path.parent.name + name = file_path.stem + + # Use cluster and name for hashing to match URL structure + # The URL is /tldr/[cluster]/[name] + hash_category = cluster + + full_hash = create_full_hash(hash_category, name) + url_hash = get_8_bytes(full_hash) + + # Update path to match the new URL structure + # This overrides the path from frontmatter which might be inconsistent + path_url = f"/freedevtools/tldr/{cluster}/{name}/" + + return { + "url_hash": url_hash, + "url": f"{hash_category}/{name}", + "cluster": cluster, + "name": name, + "platform": platform, + "title": title, + "description": description, + "more_info_url": more_info_url, + "keywords": keywords, + "features": features, + "examples": json.dumps(examples), + "raw_content": content, + "path": path_url + } + + +def process_all_files(conn: sqlite3.Connection) -> None: + """Walk through DATA_DIR and populate the database.""" + cur = conn.cursor() + + # Prepare SQL + insert_sql = """ + INSERT OR REPLACE INTO pages ( + url_hash, url, cluster, name, platform, title, description, more_info_url, + keywords, features, examples, raw_content, path + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + + files = list(DATA_DIR.glob("**/*.md")) + print(f"Found {len(files)} markdown files.") + + batch = [] + count = 0 + + for file_path in files: + data = parse_tldr_file(file_path) + if data: + batch.append(( + data["url_hash"], + data["url"], + data["cluster"], + data["name"], + data["platform"], + data["title"], + data["description"], + data["more_info_url"], + data["keywords"], + data["features"], + data["examples"], + data["raw_content"], + data["path"] + )) + + if len(batch) >= 100: + cur.executemany(insert_sql, batch) + conn.commit() + count += len(batch) + batch = [] + print(f"Processed {count} files...", end="\r") + + if batch: + cur.executemany(insert_sql, batch) + conn.commit() + count += len(batch) + + print(f"\nSuccessfully inserted {count} pages.") + + +def populate_cluster_and_overview(conn: sqlite3.Connection) -> None: + """Populate cluster and overview tables.""" + cur = conn.cursor() + + print("Populating cluster table...") + cur.execute("DELETE FROM cluster") + + # Get all clusters and counts + cur.execute(""" + SELECT cluster, COUNT(*) + FROM pages + GROUP BY cluster + """) + rows = cur.fetchall() + + batch = [] + for row in rows: + cluster_name = row[0] + count = row[1] + hash_name = hash_name_to_key(cluster_name) + batch.append((cluster_name, hash_name, count, '')) + + cur.executemany(""" + INSERT INTO cluster (name, hash_name, count, description) + VALUES (?, ?, ?, ?) + """, batch) + + print("Populating overview table...") + cur.execute("DELETE FROM overview") + cur.execute(""" + INSERT INTO overview (id, total_count) + SELECT 1, COUNT(*) FROM pages + """) + + conn.commit() + + +def main() -> None: + # Ensure output directory exists + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + + # Remove existing DB to start fresh (optional, but good for full rebuilds) + if DB_PATH.exists(): + DB_PATH.unlink() + + print(f"Creating database at {DB_PATH}") + with sqlite3.connect(DB_PATH) as conn: + ensure_schema(conn) + process_all_files(conn) + populate_cluster_and_overview(conn) + + # Verify + cur = conn.cursor() + cur.execute("SELECT total_count FROM overview") + row = cur.fetchone() + if row: + print(f"Total pages in DB: {row[0]}") + else: + print("Total pages in DB: 0") + + # Verify schema + print("\nVerifying schema for 'pages'...") + cur.execute("PRAGMA table_info(pages)") + columns = cur.fetchall() + for col in columns: + print(col) + + print("\nVerifying schema for 'cluster'...") + cur.execute("PRAGMA table_info(cluster)") + columns = cur.fetchall() + for col in columns: + print(col) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/frontend/serve/Makefile b/frontend/serve/Makefile new file mode 100644 index 0000000000..d01e0743a5 --- /dev/null +++ b/frontend/serve/Makefile @@ -0,0 +1,355 @@ + +pm2logs: + @if [ -z "$(LABEL)" ]; then \ + echo "❌ Please provide LABEL=some-name"; \ + exit 1; \ + fi + @python3 pm2_logs.py "$(LABEL)" + +pmdlogs: + @if [ -z "$(LABEL)" ]; then \ + echo "❌ Please provide LABEL=some-name"; \ + exit 1; \ + fi + @python3 pmd_logs.py "$(LABEL)" + + +test-svg-req-pm2: + clear + # npm run build + clear + rm -rf logs/* + pm2 flush + pm2 stop all + pm2 start pm2.config.cjs + pm2 ls + ./pm2-pin.sh + ./pm2-verifypin.sh + @echo "⬇️ Server up -----------" + @sleep 2 + @echo "⬇️ Warming up server logs will be flushed for the below requests, ignore the below curls -----------" + hey -z 10s -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/two-hump-camel/ + @sleep 2 + @pm2 flush + @echo "⬇️ Requesting SVG icon -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/two-hump-camel/ + @sleep 2 + @echo "⬇️ Showing logs -----------" + @LOG_FILES=$$(ls -1t ~/.pm2/logs/astro-*-out-*.log 2>/dev/null | head -n2); \ + if [ -n "$$LOG_FILES" ]; then \ + for LOG_FILE in $$LOG_FILES; do \ + echo "=== $$(basename $$LOG_FILE) ==="; \ + cat "$$LOG_FILE"; \ + echo ""; \ + done; \ + else \ + echo "No astro-out log yet."; \ + fi + @echo "" + @echo "⬇️ Showing PM2 logs -----------" + make pm2logs LABEL=test-svg-req-pm2 + pm2 stop all + pm2 delete all --force 2>/dev/null || true + +test-svg-req-pmd: + clear + # npm run build + clear + rm -rf logs/* + pmdaemon delete all --force 2>/dev/null || true + @sleep 2 + bash kill-ports.sh + @sleep 2 + bash kill-ports.sh + @sleep 2 + pmdaemon --config pmd.config.json start + @sleep 2 + pmdaemon list + ./pmd-pin.sh + ./pmd-verifypin.sh + @echo "⬇️ Server up -----------" + @sleep 2 + @echo "⬇️ Warming up server logs will be flushed for the below requests, ignore the below curls -----------" + hey -z 10s -host hexmos-local.com http://127.0.0.1/freedevtools/svg_icons/8bit/sharp-solid-alien-8bit/ + @sleep 2 + @echo "⬇️ Requesting SVG icon -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/svg_icons/8bit/sharp-solid-alien-8bit/ + @sleep 2 + @echo "⬇️ Showing logs -----------" + @LOG_FILES=$$(ls -1t ~/.pmdaemon/logs/astro-*-out.log 2>/dev/null | head -n2); \ + if [ -n "$$LOG_FILES" ]; then \ + for LOG_FILE in $$LOG_FILES; do \ + echo "=== $$(basename $$LOG_FILE) ==="; \ + cat "$$LOG_FILE"; \ + echo ""; \ + done; \ + else \ + echo "No astro-out log yet."; \ + fi + @echo "" + @echo "⬇️ Showing PMDaemon logs -----------" + make pmdlogs LABEL=test-svg-req-pmd + pmdaemon stop all + pmdaemon delete all --force 2>/dev/null || true + bash kill-ports.sh + +test-mcp-req-pmd: + clear + # npm run build + clear + rm -rf logs/* + pmdaemon delete all --force 2>/dev/null || true + @sleep 2 + bash kill-ports.sh + @sleep 2 + bash kill-ports.sh + @sleep 2 + pmdaemon --config pmd.config.json start + @sleep 2 + pmdaemon list + ./pmd-pin.sh + ./pmd-verifypin.sh + @echo "⬇️ Server up -----------" + @sleep 2 + @echo "⬇️ Warming up server logs will be flushed for the below requests, ignore the below curls -----------" + hey -n 5 -c 5 -host hexmos-local.com http://127.0.0.1/freedevtools/mcp/apis-and-http-requests/1/ + @sleep 2 + @echo "⬇️ Requesting MCP index -----------" + hey -z 5m -host hexmos-local.com http://127.0.0.1/freedevtools/mcp/1/ + @sleep 2 + @echo "⬇️ Requesting MCP category -----------" + hey -z 5m -host hexmos-local.com http://127.0.0.1/freedevtools/mcp/data-analytics/1/ + @sleep 2 + @echo "⬇️ Requesting MCP end page -----------" + hey -z 5m -host hexmos-local.com http://127.0.0.1/freedevtools/mcp/data-analytics/24mlight--a-share-mcp-is-just-i-need/ + + hey -z 10s -host hexmos-local.com http://127.0.0.1/freedevtools/tldr/adb/adb + @python3 pmd_logs.py --peek + @sleep 2 + @echo "⬇️ Requesting tldr 10sec" + hey -z 10s -host hexmos-local.com http://127.0.0.1/freedevtools/tldr/npm/ + @python3 pmd_logs.py --peek + @sleep 2 + @echo "10s Requesting tldr main" + hey -z 10s -host hexmos-local.com http://127.0.0.1/freedevtools/tldr + @python3 pmd_logs.py --peek + @sleep 2 + @echo "1min Requesting tldr 1min" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/tldr/npm/npm-fund/ + @python3 pmd_logs.py --peek + @sleep 2 + @echo "1min Requesting tldr main" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/tldr/npm + @python3 pmd_logs.py --peek + @sleep 2 + @echo "1min Requesting tldr main" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/tldr + @python3 pmd_logs.py --peek + @sleep 2 + @echo "5min Requesting tldr 5min" + hey -z 5m -host hexmos-local.com http://127.0.0.1/freedevtools/tldr/npm/npm-fund/ + @python3 pmd_logs.py --peek + @sleep 2 + @echo "⬇️ Showing logs -----------" +# @LOG_FILES=$$(ls -1t ~/.pmdaemon/logs/astro-*-out.log 2>/dev/null | head -n2); \ +# if [ -n "$$LOG_FILES" ]; then \ +# for LOG_FILE in $$LOG_FILES; do \ +# echo "=== $$(basename $$LOG_FILE) ==="; \ +# cat "$$LOG_FILE"; \ +# echo ""; \ +# done; \ +# else \ +# echo "No astro-out log yet."; \ +# fi + @echo "" + @echo "⬇️ Showing PMDaemon logs -----------" + make pmdlogs LABEL=test-mcp-req-pmd + pmdaemon stop all + pmdaemon delete all --force 2>/dev/null || true + bash kill-ports.sh + + +test-cheatsheets-req-pmd: + clear + # npm run build + clear + rm -rf logs/* + pmdaemon delete all --force 2>/dev/null || true + @sleep 2 + bash kill-ports.sh + @sleep 2 + bash kill-ports.sh + @sleep 2 + pmdaemon --config pmd.config.json start + @sleep 2 + pmdaemon list + ./pmd-pin.sh + ./pmd-verifypin.sh + @echo "⬇️ Server up -----------" + @sleep 2 + @echo "⬇️ Warming up server logs will be flushed for the below requests, ignore the below curls -----------" + hey -n 5 -c 5 -host hexmos-local.com http://127.0.0.1/freedevtools/c/ + @sleep 2 + @echo "⬇️ Requesting Cheatsheet index -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/c/ + @sleep 2 + @echo "⬇️ Requesting Cheatsheet category -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/c/security/ + @sleep 2 + @echo "⬇️ Requesting Cheatsheet end page -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/c/security/medusa/ + @sleep 2 + + @echo "⬇️ Showing logs -----------" +# @LOG_FILES=$$(ls -1t ~/.pmdaemon/logs/astro-*-out.log 2>/dev/null | head -n2); \ +# if [ -n "$$LOG_FILES" ]; then \ +# for LOG_FILE in $$LOG_FILES; do \ +# echo "=== $$(basename $$LOG_FILE) ==="; \ +# cat "$$LOG_FILE"; \ +# echo ""; \ +# done; \ +# else \ +# echo "No astro-out log yet."; \ +# fi + @echo "" + @echo "⬇️ Showing PMDaemon logs -----------" + make pmdlogs LABEL=test-mcp-req-pmd + pmdaemon stop all + pmdaemon delete all --force 2>/dev/null || true + bash kill-ports.sh + +test-man-pages-req-pmd: + clear + # npm run build + clear + rm -rf logs/* + pmdaemon delete all --force 2>/dev/null || true + @sleep 2 + bash kill-ports.sh + @sleep 2 + bash kill-ports.sh + @sleep 2 + pmdaemon --config pmd.config.json start + @sleep 2 + pmdaemon list + ./pmd-pin.sh + ./pmd-verifypin.sh + @echo "⬇️ Server up -----------" + @sleep 2 + @echo "⬇️ Warming up server logs will be flushed for the below requests, ignore the below curls -----------" + hey -n 5 -c 5 -host hexmos-local.com http://127.0.0.1/freedevtools/man-pages/ + @sleep 2 + @echo "⬇️ Requesting Cheatsheet index -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/man-pages/ + @sleep 2 + @echo "⬇️ Requesting Cheatsheet category -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/man-pages/library-functions/ + @sleep 2 + @echo "⬇️ Requesting Cheatsheet end page -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/man-pages/library-functions/cryptography/ + + @sleep 2 + @echo "⬇️ Requesting Cheatsheet end page -----------" + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/man-pages/library-functions/computer-science/pcmax1/ + + @echo "⬇️ Showing logs -----------" +# @LOG_FILES=$$(ls -1t ~/.pmdaemon/logs/astro-*-out.log 2>/dev/null | head -n2); \ +# if [ -n "$$LOG_FILES" ]; then \ +# for LOG_FILE in $$LOG_FILES; do \ +# echo "=== $$(basename $$LOG_FILE) ==="; \ +# cat "$$LOG_FILE"; \ +# echo ""; \ +# done; \ +# else \ +# echo "No astro-out log yet."; \ +# fi + @echo "" + @echo "⬇️ Showing PMDaemon logs -----------" + make pmdlogs LABEL=test-mcp-req-pmd + pmdaemon stop all + pmdaemon delete all --force 2>/dev/null || true + bash kill-ports.sh +# @echo "" + @echo "⬇️ Showing PMDaemon logs -----------" + make pmdlogs LABEL=test-svg-req-pmd +# pmdaemon stop all + pmdaemon delete all --force 2>/dev/null || true + bash kill-ports.sh + +serve: + cd .. && bun run serve-ssr + + + +curl-all-kind-of-page: + @echo "SVG Icon (localhost:4321):" + @hey -n 10 -c 1 http://localhost:4321/freedevtools/svg_icons/8bit/sharp-solid-alien-8bit/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + @echo "" + @echo "PNG Icon (localhost:4321):" + @hey -n 10 -c 1 http://localhost:4321/freedevtools/png_icons/8bit/sharp-solid-alien-8bit/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + @echo "" + @echo "Emoji (localhost:4321):" + @hey -n 10 -c 1 http://localhost:4321/freedevtools/emojis/two-hump-camel/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + @echo "" + @echo "PNG Icon (nginx:127.0.0.1):" + @hey -n 10 -c 1 -host hexmos-local.com http://127.0.0.1/freedevtools/png_icons/8bit/sharp-solid-alien-8bit/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + @echo "" + @echo "SVG Icon (nginx:127.0.0.1):" + @hey -n 10 -c 1 -host hexmos-local.com http://127.0.0.1/freedevtools/svg_icons/8bit/sharp-solid-alien-8bit/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + @echo "" + @echo "Emoji (nginx:127.0.0.1):" + @hey -n 10 -c 1 -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/two-hump-camel/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + @echo "" + @echo "Emoji (localhost:4321):" + @hey -n 10 -c 1 -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + @echo "Emoji category (localhost:4321):" + @hey -n 10 -c 1 -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/animals-nature/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + @echo "Emoji end page (localhost:4321):" + @hey -n 10 -c 1 -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/two-hump-camel/ 2>&1 | grep -A 10 "Status code distribution" || echo " No status codes found" + + + +test-emoji-req-pmd: + clear + # npm run build + clear + rm -rf logs/* + pmdaemon delete all --force 2>/dev/null || true + @sleep 2 + bash kill-ports.sh + @sleep 2 + bash kill-ports.sh + @sleep 2 + pmdaemon --config pmd.config.json start + @sleep 2 + pmdaemon list + ./pmd-pin.sh + ./pmd-verifypin.sh + @echo "⬇️ Server up -----------" + @sleep 2 + @echo "⬇️ Warming up server logs will be flushed for the below requests, ignore the below curls -----------" + # hey -z 10s -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/two-hump-camel/ + hey -z 10s -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/ + pmdaemon logs astro-4321 + @sleep 2 + @echo "⬇️ Requesting SVG icon -----------" + # hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/two-hump-camel/ + hey -z 1m -host hexmos-local.com http://127.0.0.1/freedevtools/emojis/ + @sleep 2 + @echo "⬇️ Showing logs -----------" + @LOG_FILES=$$(ls -1t ~/.pmdaemon/logs/astro-*-out.log 2>/dev/null | head -n2); \ + if [ -n "$$LOG_FILES" ]; then \ + for LOG_FILE in $$LOG_FILES; do \ + echo "=== $$(basename $$LOG_FILE) ==="; \ + cat "$$LOG_FILE"; \ + echo ""; \ + done; \ + else \ + echo "No astro-out log yet."; \ + fi + @echo "" + @echo "⬇️ Showing PMDaemon logs -----------" + make pmdlogs LABEL=test-svg-req-pmd + pmdaemon stop all + pmdaemon delete all --force 2>/dev/null || true + bash kill-ports.sh diff --git a/frontend/serve/astro-4322 b/frontend/serve/astro-4322 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/serve/kill-ports.sh b/frontend/serve/kill-ports.sh new file mode 100755 index 0000000000..f344d14108 --- /dev/null +++ b/frontend/serve/kill-ports.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +PORTS=(4321 4322) + +for PORT in "${PORTS[@]}"; do + echo "Checking port $PORT..." + PIDS=$(lsof -ti :$PORT 2>/dev/null) + + if [ -z "$PIDS" ]; then + echo " No process found on port $PORT" + else + for PID in $PIDS; do + echo " Killing PID $PID on port $PORT" + kill -9 $PID 2>/dev/null + done + echo " Port $PORT cleared" + fi +done + +echo "Done." + diff --git a/frontend/serve/pm2-pin.sh b/frontend/serve/pm2-pin.sh new file mode 100755 index 0000000000..73613f6640 --- /dev/null +++ b/frontend/serve/pm2-pin.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +APPS=("astro-4321" "astro-4322") + +# Wait until PM2 workers are ready +for APP in "${APPS[@]}"; do + while [ -z "$(pm2 pid $APP 2>/dev/null)" ]; do + sleep 2 + done +done + +i=0 +for APP in "${APPS[@]}"; do + for pid in $(pm2 pid $APP 2>/dev/null); do + echo "Pinning PID $pid ($APP) to CPU $i" + taskset -pc $i $pid + i=$((i+1)) + done +done + diff --git a/frontend/serve/pm2-verifypin.sh b/frontend/serve/pm2-verifypin.sh new file mode 100755 index 0000000000..a5823ff716 --- /dev/null +++ b/frontend/serve/pm2-verifypin.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +APPS=("astro-4321" "astro-4322") + +echo "Checking CPU pinning for PM2 apps: ${APPS[*]}" +echo "--------------------------------------" + +for APP in "${APPS[@]}"; do + PIDS=$(pm2 pid "$APP" 2>/dev/null) + + if [ -z "$PIDS" ]; then + echo "No PIDs found for $APP. App might be stopped." + continue + fi + + echo "App: $APP" + for pid in $PIDS; do + affinity=$(taskset -pc $pid 2>&1 | grep "affinity") + echo " PID: $pid | $affinity" + done + echo "" +done + +echo "--------------------------------------" +echo "Done." + diff --git a/frontend/serve/pm2.config.cjs b/frontend/serve/pm2.config.cjs new file mode 100644 index 0000000000..495a08c576 --- /dev/null +++ b/frontend/serve/pm2.config.cjs @@ -0,0 +1,26 @@ +module.exports = { + apps: [ + { + name: "astro-4321", + script: "start-server.sh", + instances: 1, + exec_mode: "fork", + env: { + PATH: "/home/lovestaco/.bun/bin:" + (process.env.PATH || ""), + UV_THREADPOOL_SIZE: 64, + PORT: 4321 + } + }, + { + name: "astro-4322", + script: "start-server.sh", + instances: 1, + exec_mode: "fork", + env: { + PATH: "/home/lovestaco/.bun/bin:" + (process.env.PATH || ""), + UV_THREADPOOL_SIZE: 64, + PORT: 4322 + } + } + ] +}; diff --git a/frontend/serve/pm2_logs.py b/frontend/serve/pm2_logs.py new file mode 100644 index 0000000000..61b9740329 --- /dev/null +++ b/frontend/serve/pm2_logs.py @@ -0,0 +1,456 @@ +""" +Copy PM2 logs for the two `astro` processes and summarize key events. + +Usage: python scripts/pm2_logs.py
{!cheatsheetCategory && } - diff --git a/frontend/src/components/banner/Banner.astro b/frontend/src/components/banner/Banner.astro index 7bad7016b7..b77ca46b11 100644 --- a/frontend/src/components/banner/Banner.astro +++ b/frontend/src/components/banner/Banner.astro @@ -12,6 +12,9 @@ interface Props { const { type } = Astro.props; +// Check if ads are enabled via environment variable +const adsEnabled = import.meta.env.ENABLE_ADS === 'true'; + // AdSense HTML for banner type - Laptop/Desktop const adsenseHtmlLaptop = ` @@ -50,7 +53,6 @@ const adsenseHtmlMobile = ` -
+{ + adsEnabled ? ( +
+ ) : ( +
+ ) +} diff --git a/frontend/src/content.config.ts b/frontend/src/content.config.ts index d10aefa8ed..3951ed9b99 100644 --- a/frontend/src/content.config.ts +++ b/frontend/src/content.config.ts @@ -6,154 +6,83 @@ import { defineCollection, z } from 'astro:content'; const forceTldrBuild = true; // Define the tldr collection schema based on the frontmatter structure -const tldr = defineCollection({ - loader: glob({ - // In dev mode, only load specific categories; in build mode, load all files - pattern: forceTldrBuild ? '**/*.md' : '{pnm,git}/**/*.md', - base: 'data/tldr', - }), - schema: z.object({ - title: z.string(), - name: z.string(), - path: z.string(), - canonical: z.string().url(), - description: z.string(), - category: z.string(), - keywords: z.array(z.string()).optional(), - features: z.array(z.string()).optional(), - ogImage: z.string().url().optional(), - twitterImage: z.string().url().optional(), - relatedTools: z - .array( - z.object({ - name: z.string(), - url: z.string().url(), - banner: z.string().url().optional(), - }) - ) - .optional(), - more_information: z.string().url().optional(), - }), -}); - -// Define the MCP metadata collection -const mcpMetadata = defineCollection({ - loader: file('public/mcp/meta_data.json', { - parser: (fileContent) => { - const data = JSON.parse(fileContent); - return { - 'mcp-metadata': data, - }; - }, - }), - schema: z.object({ - totalCategories: z.number(), - totalRepositories: z.number(), - processing_started: z.string(), - processing_completed: z.string(), - categories: z.record( - z.string(), - z.object({ - categoryDisplay: z.string(), - totalRepositories: z.number(), - totalStars: z.number(), - totalForks: z.number(), - npmPackages: z.number(), - npmDownloads: z.number(), - }) - ), - summary: z.object({ - totalStars: z.number(), - totalForks: z.number(), - npmPackages: z.number(), - npmDownloads: z.number(), - }), - }), -}); - -// Define the MCP category data collection using glob loader -const mcpCategoryData = defineCollection({ - loader: glob({ - pattern: '**/*.json', - base: './public/mcp/input', - }), - schema: z.object({ - category: z.string(), - categoryDisplay: z.string(), - description: z.string(), - totalRepositories: z.number(), - repositories: z.record( - z.string(), - z.object({ - owner: z.string(), - name: z.string(), - url: z.string().url(), - imageUrl: z.string().optional(), - description: z.string().optional(), - stars: z.number(), - forks: z.number(), - license: z.string(), - language: z.string(), - updated_at: z.string(), - readme_content: z.string().optional(), - npm_url: z.string(), - npm_downloads: z.number(), - keywords: z.array(z.string()).optional(), - category: z.string(), // Add category field to repository schema - }) - ), - }), -}); +// const tldr = defineCollection({ +// loader: glob({ +// // In dev mode, only load specific categories; in build mode, load all files +// pattern: forceTldrBuild ? '**/*.md' : '{pnm,git}/**/*.md', +// base: 'data/tldr', +// }), +// schema: z.object({ +// title: z.string(), +// name: z.string(), +// path: z.string(), +// canonical: z.string().url(), +// description: z.string(), +// category: z.string(), +// keywords: z.array(z.string()).optional(), +// features: z.array(z.string()).optional(), +// ogImage: z.string().url().optional(), +// twitterImage: z.string().url().optional(), +// relatedTools: z +// .array( +// z.object({ +// name: z.string(), +// url: z.string().url(), +// banner: z.string().url().optional(), +// }) +// ) +// .optional(), +// more_information: z.string().url().optional(), +// }), +// }); // Define the PNG icons metadata collection -const pngIconsMetadata = defineCollection({ - loader: file('data/cluster_png.json', { - parser: (fileContent) => { - const data = JSON.parse(fileContent); - return { - 'png-icons-metadata': data, - }; - }, - }), - schema: z.object({ - clusters: z.record( - z.string(), - z.object({ - name: z.string(), - source_folder: z.string(), - path: z.string(), - keywords: z.array(z.string()), - features: z.array(z.string()), - title: z.string(), - description: z.string(), - fileNames: z.array( - z.union([ - z.string(), // Simple filename - z - .object({ - fileName: z.string(), - description: z.string().optional(), - usecases: z.string().optional(), - synonyms: z.array(z.string()).optional(), - tags: z.array(z.string()).optional(), - industry: z.string().optional(), - emotional_cues: z.string().optional(), - enhanced: z.boolean().optional(), - author: z.string().optional(), - license: z.string().optional(), - }) - .passthrough(), // Allow additional properties - ]) - ), - enhanced: z.boolean().optional(), - }) - ), - }), -}); +// const pngIconsMetadata = defineCollection({ +// loader: file('data/cluster_png.json', { +// parser: (fileContent) => { +// const data = JSON.parse(fileContent); +// return { +// 'png-icons-metadata': data, +// }; +// }, +// }), +// schema: z.object({ +// clusters: z.record( +// z.string(), +// z.object({ +// name: z.string(), +// source_folder: z.string(), +// path: z.string(), +// keywords: z.array(z.string()), +// features: z.array(z.string()), +// title: z.string(), +// description: z.string(), +// fileNames: z.array( +// z.union([ +// z.string(), // Simple filename +// z +// .object({ +// fileName: z.string(), +// description: z.string().optional(), +// usecases: z.string().optional(), +// synonyms: z.array(z.string()).optional(), +// tags: z.array(z.string()).optional(), +// industry: z.string().optional(), +// emotional_cues: z.string().optional(), +// enhanced: z.boolean().optional(), +// author: z.string().optional(), +// license: z.string().optional(), +// }) +// .passthrough(), // Allow additional properties +// ]) +// ), +// enhanced: z.boolean().optional(), +// }) +// ), +// }), +// }); -export const collections = { - tldr, - mcpMetadata, - mcpCategoryData, - pngIconsMetadata, -}; +// export const collections = { +// tldr, +// pngIconsMetadata, +// }; diff --git a/frontend/src/layouts/BaseLayout.astro b/frontend/src/layouts/BaseLayout.astro index cf2df209ff..c54695b7a4 100644 --- a/frontend/src/layouts/BaseLayout.astro +++ b/frontend/src/layouts/BaseLayout.astro @@ -3,6 +3,7 @@ import Header from '../components/Header.astro'; import Footer from '../components/Footer.astro'; import '../styles/global.css'; import SearchPage from '../components/search/SearchPage'; +import RequireAuth from '../components/auth/RequireAuth.astro'; // Rest of imports and props remain the same export interface Props { // Basic SEO @@ -86,10 +87,16 @@ const { thumbnailUrl, encodingFormat = 'text/html', } = Astro.props; +console.log( + `[BASE_LAYOUT] Start rendering ${name} at ${new Date().toISOString()}` +); const currentUrl = canonical || Astro.url.href; const keywordsString = keywords.length > 0 ? keywords.join(', ') : ''; +// Check if ads are enabled via environment variable +const adsEnabled = import.meta.env.ENABLE_ADS === 'true'; + // Determine page type based on path and props function determinePageType(path: string, props: any): string { if (props.pageType) return props.pageType; @@ -397,7 +404,11 @@ const schema = generatePageSpecificSchema(detectedPageType, Astro.props); - + { + adsEnabled && ( + + ) + } = { - 'Smileys & Emotion': '😀', - 'People & Body': '👤', - 'Animals & Nature': '🐶', - 'Food & Drink': '🍎', - 'Travel & Places': '✈️', - Activities: '⚽', - Objects: '📱', - Symbols: '❤️', - Flags: '🏁', - Other: '❓', -}; - -// === SQLite connection handling === -let dbInstance: Database.Database | null = null; - -function getDbPath(): string { - return path.resolve(process.cwd(), 'db/all_dbs/emoji-db.db'); -} - -export function getDb(): Database.Database { - if (dbInstance) return dbInstance; - const dbPath = getDbPath(); - dbInstance = new Database(dbPath, { readonly: true }); - dbInstance.pragma('journal_mode = OFF'); - dbInstance.pragma('synchronous = OFF'); - return dbInstance; -} - -// === Utility: Safe JSON parse === -function parseJSON(value: string | null): T | undefined { - if (!value) return undefined; - try { - return JSON.parse(value); - } catch { - return undefined; - } -} - -// === Fetch all emojis === -export function getAllEmojis(): EmojiData[] { - const db = getDb(); - const rows = db - .prepare( - ` - SELECT - code, - unicode, - slug, - title, - category, - description, - apple_vendor_description, - keywords, - also_known_as, - version, - senses, - shortcodes - FROM emojis - ` - ) - .all(); - - const emojis: EmojiData[] = rows.map((r) => ({ - code: r.code, - slug: r.slug, - title: r.title, - description: r.description, - category: r.category, - apple_vendor_description: r.apple_vendor_description, - Unicode: parseJSON(r.unicode) || [], - keywords: parseJSON(r.keywords) || [], - alsoKnownAs: parseJSON(r.also_known_as) || [], - version: parseJSON(r.version), - senses: parseJSON(r.senses), - shortcodes: parseJSON(r.shortcodes), - })); - - // Sort: base first, then tone variants - const toneRegex = /(light|medium|dark)?-?skin-tone/; - emojis.sort((a, b) => { - const aTone = toneRegex.test(a.slug); - const bTone = toneRegex.test(b.slug); - if (aTone && !bTone) return 1; - if (!aTone && bTone) return -1; - return (a.title || a.slug).localeCompare(b.title || b.slug); - }); - - return emojis; -} - -// === Fetch single emoji === -export function getEmojiBySlug(slug: string): EmojiData | null { - const db = getDb(); - const row = db.prepare(`SELECT * FROM emojis WHERE slug = ?`).get(slug); - if (!row) return null; - - return { - code: row.code, - slug: row.slug, - title: row.title, - description: row.description, - category: row.category, - apple_vendor_description: row.apple_vendor_description, - discord_vendor_description: row.discord_vendor_description, - Unicode: parseJSON(row.unicode) || [], - keywords: parseJSON(row.keywords) || [], - alsoKnownAs: parseJSON(row.also_known_as) || [], - version: parseJSON(row.version), - senses: parseJSON(row.senses), - shortcodes: parseJSON(row.shortcodes), - }; -} - -// === Fetch categories === -export function getEmojiCategories(): string[] { - const db = getDb(); - const rows = db - .prepare(`SELECT DISTINCT category FROM emojis WHERE category IS NOT NULL`) - .all(); - - const validCategories = Object.keys(categoryIconMap); - const normalized = rows.map((r) => - validCategories.includes(r.category) ? r.category : 'Other' - ); - - return Array.from(new Set(normalized)).sort(); -} - -// === Fetch by category === -export function getEmojisByCategory(category: string, vendor?: string): EmojiData[] { - const db = getDb(); - - const rows = db - .prepare(`SELECT * FROM emojis WHERE lower(category) = lower(?)`) - .all(category); - - return rows - .filter(r => { - if (vendor === "discord") { - return !discord_vendor_excluded_emojis.includes(r.slug); - } - if (vendor === "apple") { - return !apple_vendor_excluded_emojis.includes(r.slug); - } - return true; - }) - .map((r) => ({ - code: r.code, - slug: r.slug, - title: r.title, - description: r.description, - category: r.category, - apple_vendor_description: r.apple_vendor_description, - Unicode: parseJSON(r.unicode) || [], - keywords: parseJSON(r.keywords) || [], - alsoKnownAs: parseJSON(r.also_known_as) || [], - version: parseJSON(r.version), - senses: parseJSON(r.senses), - shortcodes: parseJSON(r.shortcodes), - })); -} - - -export function getEmojiImages(slug) { - const db = getDb(); - const rows = db - .prepare(`SELECT filename, image_data FROM images WHERE emoji_slug = ?`) - .all(slug); - - const images = {}; - - for (const row of rows) { - const lower = row.filename.toLowerCase(); - - const setImage = (variant) => { - if (images[variant]) return; - - const buffer = Buffer.from(row.image_data); - let mime = 'application/octet-stream'; - - // --- Detect type from header instead of extension --- - const head = buffer.slice(0, 20).toString('utf8'); - - if (head.startsWith(' /iOS[_\s]?\d+/i.test(row.filename)) // only Apple evolution ones - .map((row) => { - const buffer = Buffer.from(row.image_data); - const mime = detectMime(buffer); - const base64 = buffer.toString('base64'); - - return { - file: row.filename, - url: `data:${mime};base64,${base64}`, - version: extractIOSVersion(row.filename), - }; - }) - .sort((a, b) => { - const va = versionToNumbers(a.version); - const vb = versionToNumbers(b.version); - const len = Math.max(va.length, vb.length); - for (let i = 0; i < len; i++) { - const diff = (va[i] || 0) - (vb[i] || 0); - if (diff !== 0) return diff; - } - return 0; - }); - - if (appleImages.length === 0) continue; - - const latestImage = appleImages[appleImages.length - 1]; - - allEmojis.push({ - ...emoji, - Unicode: parseJSON(emoji.unicode) || [], - keywords: parseJSON(emoji.keywords) || [], - alsoKnownAs: parseJSON(emoji.also_known_as) || [], - version: parseJSON(emoji.version), - senses: parseJSON(emoji.senses), - shortcodes: parseJSON(emoji.shortcodes), - slug, - appleEvolutionImages: appleImages, - latestAppleImage: latestImage.url, - apple_vendor_description: - emoji.apple_vendor_description || emoji.description || '', - }); - } - - return allEmojis; -} - - -export function getAllDiscordEmojis() { - const db = getDb(); - - const emojiRows = db - .prepare( - `SELECT - code, - unicode, - slug, - title, - category, - description, - apple_vendor_description, - discord_vendor_description, - keywords, - also_known_as, - version, - senses, - shortcodes - FROM emojis` - ) - .all(); - - const allEmojis = []; - - for (const emoji of emojiRows) { - const slug = emoji.slug; - - // Get only Discord vendor images - const imageRows = db - .prepare( - `SELECT filename, image_data - FROM images - WHERE emoji_slug = ? AND image_type = 'twemoji-vendor'` - ) - .all(slug); - - // Filter and normalize Discord evolution images - const discordImages = imageRows - .map((row) => { - const buffer = Buffer.from(row.image_data); - const mime = detectMime(buffer); - const base64 = buffer.toString('base64'); - - return { - file: row.filename, - url: `data:${mime};base64,${base64}`, - version: extractDiscordVersion(row.filename), - }; - }) - .sort((a, b) => { - const va = versionToNumbers(a.version); - const vb = versionToNumbers(b.version); - const len = Math.max(va.length, vb.length); - for (let i = 0; i < len; i++) { - const diff = (va[i] || 0) - (vb[i] || 0); - if (diff !== 0) return diff; - } - return 0; - }); - - if (discordImages.length === 0) continue; - - const latestImage = discordImages[discordImages.length - 1]; - - allEmojis.push({ - ...emoji, - Unicode: parseJSON(emoji.unicode) || [], - keywords: parseJSON(emoji.keywords) || [], - alsoKnownAs: parseJSON(emoji.also_known_as) || [], - version: parseJSON(emoji.version), - senses: parseJSON(emoji.senses), - shortcodes: parseJSON(emoji.shortcodes), - slug, - discordEvolutionImages: discordImages, - latestDiscordImage: latestImage.url, - discord_vendor_description: - emoji.discord_vendor_description || emoji.description || '', - }); - } - - return allEmojis; -} - -export function extractDiscordVersion(filename) { - // Matches _7.0.png or -14.1.webp etc. - const match = filename.match(/[_-]([\d.]+)\.(png|jpg|jpeg|webp|svg)$/i); - return match ? match[1] : '0'; -} - - -export function getDiscordEmojiBySlug(slug: string) { - const db = getDb(); - - const emoji = db - .prepare( - `SELECT - code, - unicode, - slug, - title, - category, - description, - apple_vendor_description, - discord_vendor_description, - keywords, - also_known_as, - version, - senses, - shortcodes - FROM emojis - WHERE slug = ?` - ) - .get(slug); - - if (!emoji) return null; - - // Get only Discord-vendor images - const imageRows = db - .prepare( - `SELECT filename, image_data - FROM images - WHERE emoji_slug = ? AND image_type = 'twemoji-vendor'` - ) - .all(slug); - - const discordImages = imageRows - .map((row) => { - const buffer = Buffer.from(row.image_data); - const mime = detectMime(buffer); - const base64 = buffer.toString("base64"); - - return { - file: row.filename, - url: `data:${mime};base64,${base64}`, - version: extractDiscordVersion(row.filename), - }; - }) - .sort((a, b) => { - const va = versionToNumbers(a.version); - const vb = versionToNumbers(b.version); - const len = Math.max(va.length, vb.length); - for (let i = 0; i < len; i++) { - const diff = (va[i] || 0) - (vb[i] || 0); - if (diff !== 0) return diff; - } - return 0; - }); - - if (discordImages.length === 0) return null; - - const latestImage = discordImages[discordImages.length - 1]; - - return { - ...emoji, - Unicode: parseJSON(emoji.unicode) || [], - keywords: parseJSON(emoji.keywords) || [], - alsoKnownAs: parseJSON(emoji.also_known_as) || [], - version: parseJSON(emoji.version), - senses: parseJSON(emoji.senses), - shortcodes: parseJSON(emoji.shortcodes), - slug, - discordEvolutionImages: discordImages, - latestDiscordImage: latestImage.url, - discord_vendor_description: - emoji.discord_vendor_description || emoji.description || "", - }; -} - - -export function getAppleEmojiBySlug(slug: string) { - const db = getDb(); - - // fetch base row - const emoji = db - .prepare( - `SELECT - code, - unicode, - slug, - title, - category, - description, - apple_vendor_description, - discord_vendor_description, - keywords, - also_known_as, - version, - senses, - shortcodes - FROM emojis - WHERE slug = ?` - ) - .get(slug); - - if (!emoji) return null; - - // fetch all images for this emoji only - const imageRows = db - .prepare(`SELECT filename, image_data FROM images WHERE emoji_slug = ?`) - .all(slug); - - const appleImages = imageRows - .filter((row) => /iOS[_\s]?\d+/i.test(row.filename)) - .map((row) => { - const buffer = Buffer.from(row.image_data); - const mime = detectMime(buffer); - return { - file: row.filename, - url: `data:${mime};base64,${buffer.toString("base64")}`, - version: extractIOSVersion(row.filename), - }; - }) - .sort((a, b) => { - const va = versionToNumbers(a.version); - const vb = versionToNumbers(b.version); - const len = Math.max(va.length, vb.length); - for (let i = 0; i < len; i++) { - const diff = (va[i] || 0) - (vb[i] || 0); - if (diff !== 0) return diff; - } - return 0; - }); - - const latestImage = appleImages[appleImages.length - 1] || null; - - return { - ...emoji, - Unicode: parseJSON(emoji.unicode) || [], - keywords: parseJSON(emoji.keywords) || [], - alsoKnownAs: parseJSON(emoji.also_known_as) || [], - version: parseJSON(emoji.version), - senses: parseJSON(emoji.senses), - shortcodes: parseJSON(emoji.shortcodes), - slug, - - appleEvolutionImages: appleImages, - latestAppleImage: latestImage?.url, - apple_vendor_description: - emoji.apple_vendor_description || emoji.description || "", - }; -} - - - -export function fetchImageFromDB( - slug: string, - filename: string -): string | null { - const db = getDb(); - const row = db - .prepare( - `SELECT image_data FROM images WHERE emoji_slug = ? AND filename = ?` - ) - .get(slug, filename); - - if (!row || !row.image_data) return null; - - const buffer = Buffer.from(row.image_data); - const head = buffer.toString('ascii', 0, 20); - let mime = 'application/octet-stream'; - if (head.includes(' { - const match = name.match(/iOS[_\s]?([\d.]+)/i); - return match ? parseFloat(match[1]) : 0; - }; - - // Pick the image with the highest iOS version number - const latest = rows.reduce((best, row) => { - return parseVersion(row.filename) > parseVersion(best.filename) - ? row - : best; - }, rows[0]); - - // Convert to Base64 with proper MIME detection - const buffer = Buffer.from(latest.image_data); - const head = buffer.toString('ascii', 0, 20); - let mime = 'application/octet-stream'; - if (head.includes(' { - const match = name.match(/[_-]([\d.]+)\.(png|jpg|jpeg|webp|svg)$/i); - return match ? parseFloat(match[1]) : 0; - }; - - // Pick file with highest vendor version - const latest = rows.reduce((best, row) => { - return parseVersion(row.filename) > parseVersion(best.filename) - ? row - : best; - }, rows[0]); - - // Convert BLOB to Base64 with MIME detection - const buffer = Buffer.from(latest.image_data); - const head = buffer.toString('ascii', 0, 20); - - let mime = 'application/octet-stream'; - if (head.includes(' typeof segment === 'string' && segment.length > 0) + .map((segment) => encodeURIComponent(segment)); + return '/' + segments.join('/'); +} + +export function hashUrlToKey(url: string): string { + const hash = crypto.createHash('sha256').update(url).digest(); + return hash.readBigInt64BE(0).toString(); +} + +export function hashNameToKey(name: string): string { + const hash = crypto.createHash('sha256').update(name).digest(); + return hash.readBigInt64BE(0).toString(); +} diff --git a/frontend/src/lib/man-pages-utils.ts b/frontend/src/lib/man-pages-utils.ts deleted file mode 100644 index 0500fadb0b..0000000000 --- a/frontend/src/lib/man-pages-utils.ts +++ /dev/null @@ -1,410 +0,0 @@ -import Database from 'better-sqlite3'; -import path from 'path'; -import type { ManPage, Category, SubCategory, RawManPageRow, RawCategoryRow, RawSubCategoryRow } from '../../db/man_pages/man-pages-schema'; - -// DB queries -let dbInstance: any = null; - -function getDbPath(): string { - return path.resolve(process.cwd(), 'db/all_dbs/man-pages-db.db'); -} - -export function getDb() { - if (dbInstance) return dbInstance; - const dbPath = getDbPath(); - dbInstance = new Database(dbPath, { readonly: true }); - // Improve read performance for build-time queries - dbInstance.pragma('journal_mode = OFF'); - dbInstance.pragma('synchronous = OFF'); - return dbInstance; -} - -export interface ManPageCategory { - category: string; - count: number; - description: string; -} - -// Get all man page categories with their descriptions -export function getManPageCategories(): ManPageCategory[] { - const db = getDb(); - - const stmt = db.prepare(` - SELECT name, count, description - FROM category - ORDER BY name - `); - - const results = stmt.all() as Array<{ - name: string; - count: number; - description: string; - }>; - - return results.map(row => ({ - category: row.name, - count: row.count, - description: row.description - })); -} - -// Get categories from the category table -export function getCategories(): Category[] { - const db = getDb(); - const stmt = db.prepare(` - SELECT name, count, description, - json(keywords) as keywords, path - FROM category - ORDER BY name - `); - - const results = stmt.all() as RawCategoryRow[]; - return results.map((row) => ({ - ...row, - keywords: JSON.parse(row.keywords || '[]'), - })) as Category[]; -} - -// Get subcategories from the sub_category table -export function getSubCategories(): SubCategory[] { - const db = getDb(); - const stmt = db.prepare(` - SELECT name, count, description, - json(keywords) as keywords, path - FROM sub_category - ORDER BY name - `); - - const results = stmt.all() as RawSubCategoryRow[]; - return results.map((row) => ({ - ...row, - keywords: JSON.parse(row.keywords || '[]'), - })) as SubCategory[]; -} - -// Get overview data from the overview table -export function getOverview(): Overview | null { - const db = getDb(); - const stmt = db.prepare(` - SELECT id, total_count - FROM overview - WHERE id = 1 - `); - - const result = stmt.get() as RawOverviewRow | undefined; - if (!result) return null; - - return { - id: result.id, - total_count: result.total_count, - } as Overview; -} -export function getSubCategoriesByMainCategory(mainCategory: string): SubCategory[] { - const db = getDb(); - const stmt = db.prepare(` - SELECT DISTINCT sc.name, sc.count, sc.description, - json(sc.keywords) as keywords, sc.path - FROM sub_category sc - INNER JOIN man_pages mp ON mp.sub_category = sc.name - WHERE mp.main_category = ? - ORDER BY sc.name - `); - - const results = stmt.all(mainCategory) as RawSubCategoryRow[]; - return results.map((row) => ({ - ...row, - keywords: JSON.parse(row.keywords || '[]'), - })) as SubCategory[]; -} - -// Get man pages by category -export function getManPagesByCategory(category: string): ManPage[] { - const db = getDb(); - const stmt = db.prepare(` - SELECT id, main_category, sub_category, title, slug, filename, - json(content) as content - FROM man_pages - WHERE main_category = ? - ORDER BY title - `); - - const results = stmt.all(category) as RawManPageRow[]; - return results.map((row) => ({ - ...row, - content: JSON.parse(row.content || '{}'), - })) as ManPage[]; -} - -// Get man pages by category and subcategory -export function getManPagesBySubcategory(category: string, subcategory: string): ManPage[] { - const db = getDb(); - const stmt = db.prepare(` - SELECT id, main_category, sub_category, title, slug, filename, - json(content) as content - FROM man_pages - WHERE main_category = ? AND sub_category = ? - ORDER BY title - `); - - const results = stmt.all(category, subcategory) as RawManPageRow[]; - return results.map((row) => ({ - ...row, - content: JSON.parse(row.content || '{}'), - })) as ManPage[]; -} - -// Get single man page by ID -export function getManPageById(id: string | number): ManPage | null { - const db = getDb(); - const stmt = db.prepare(` - SELECT id, main_category, sub_category, title, slug, filename, - json(content) as content - FROM man_pages - WHERE id = ? - `); - - const result = stmt.get(id) as RawManPageRow | undefined; - if (!result) return null; - - return { - ...result, - content: JSON.parse(result.content || '{}'), - } as ManPage; -} - -// Generate static paths for all man pages -export function generateManPageStaticPaths() { - const db = getDb(); - const stmt = db.prepare(` - SELECT id, main_category, sub_category - FROM man_pages - `); - - const rows = stmt.all() as Array<{ - id: number; - main_category: string; - sub_category: string; - }>; - - return rows.map(row => ({ - params: { - category: row.main_category, - subcategory: row.sub_category, - page: row.id.toString() - } - })); -} - -// Generate static paths for categories -export function generateCategoryStaticPaths() { - const db = getDb(); - const stmt = db.prepare(` - SELECT name - FROM category - `); - - const rows = stmt.all() as Array<{ name: string }>; - - return rows.map(row => ({ - params: { - category: row.name - } - })); -} - -// Generate static paths for subcategories -export function generateSubcategoryStaticPaths() { - const db = getDb(); - const stmt = db.prepare(` - SELECT DISTINCT mp.main_category, mp.sub_category - FROM man_pages mp - `); - - const rows = stmt.all() as Array<{ - main_category: string; - sub_category: string; - }>; - - return rows.map(row => ({ - params: { - category: row.main_category, - subcategory: row.sub_category - } - })); -} - -// Get man page by category, subcategory and filename -export function getManPageByPath(category: string, subcategory: string, filename: string): ManPage | null { - const db = getDb(); - const stmt = db.prepare(` - SELECT id, main_category, sub_category, title, filename, - json(content) as content - FROM man_pages - WHERE main_category = ? AND sub_category = ? AND (filename = ? OR id = ?) - `); - - const result = stmt.get(category, subcategory, filename, parseInt(filename) || 0) as RawManPageRow | undefined; - if (!result) return null; - - return { - ...result, - content: JSON.parse(result.content || '{}'), - } as ManPage; -} - -// Get man page by command name (first part of title) -export function getManPageByCommandName(category: string, subcategory: string, commandName: string): ManPage | null { - const db = getDb(); - const stmt = db.prepare(` - SELECT id, main_category, sub_category, title, slug, filename, - json(content) as content - FROM man_pages - WHERE main_category = ? AND sub_category = ? AND slug = ? - `); - - const result = stmt.get(category, subcategory, commandName) as RawManPageRow | undefined; - if (!result) return null; - - return { - ...result, - content: JSON.parse(result.content || '{}'), - } as ManPage; -} - -// Alias for better naming - get man page by slug -export function getManPageBySlug(category: string, subcategory: string, slug: string): ManPage | null { - return getManPageByCommandName(category, subcategory, slug); -} - -// Generate static paths for individual man pages using command parameter -export function generateCommandStaticPaths(): Array<{ params: { category: string; subcategory: string; slug: string } }> { - const db = getDb(); - const stmt = db.prepare(` - SELECT main_category, sub_category, slug - FROM man_pages - ORDER BY main_category, sub_category, slug - `); - - const manPages = stmt.all() as Array<{ - main_category: string; - sub_category: string; - slug: string; - }>; - - console.log(`📊 Found ${manPages.length} total man pages in database`); - - const paths = manPages.map(manPage => ({ - params: { - category: manPage.main_category, - subcategory: manPage.sub_category, - slug: manPage.slug - } - })); - - // Log some examples - if (paths.length > 0) { - console.log('🔍 Sample generated paths:', paths.slice(0, 5)); - } - - return paths; -} - -// Efficient paginated queries for better performance - -export function getSubCategoriesCountByMainCategory(mainCategory: string): number { - const db = getDb(); - const stmt = db.prepare(` - SELECT COUNT(DISTINCT sub_category) as count - FROM man_pages - WHERE main_category = ? - `); - - const result = stmt.get(mainCategory) as { count: number } | undefined; - return result?.count || 0; -} - -export function getSubCategoriesByMainCategoryPaginated( - mainCategory: string, - limit: number, - offset: number -): SubCategory[] { - const db = getDb(); - const stmt = db.prepare(` - SELECT - sub_category as name, - COUNT(*) as count, - 'Subcategory for ' || sub_category as description, - json_array(sub_category) as keywords, - '/' || sub_category as path - FROM man_pages - WHERE main_category = ? - GROUP BY sub_category - ORDER BY sub_category - LIMIT ? OFFSET ? - `); - - return stmt.all(mainCategory, limit, offset) as SubCategory[]; -} - -export function getTotalManPagesCountByMainCategory(mainCategory: string): number { - const db = getDb(); - const stmt = db.prepare(` - SELECT COUNT(*) as count - FROM man_pages - WHERE main_category = ? - `); - - const result = stmt.get(mainCategory) as { count: number } | undefined; - return result?.count || 0; -} - -export function getManPagesBySubcategoryPaginated( - mainCategory: string, - subCategory: string, - limit: number, - offset: number -): ManPage[] { - const db = getDb(); - const stmt = db.prepare(` - SELECT id, main_category, sub_category, title, slug, filename, content - FROM man_pages - WHERE main_category = ? AND sub_category = ? - ORDER BY title - LIMIT ? OFFSET ? - `); - - const rows = stmt.all(mainCategory, subCategory, limit, offset) as Array<{ - id: number; - main_category: string; - sub_category: string; - title: string; - slug: string; - filename: string; - content: string; - }>; - - return rows.map(row => ({ - id: row.id, - main_category: row.main_category, - sub_category: row.sub_category, - title: row.title, - slug: row.slug, - filename: row.filename, - content: JSON.parse(row.content) - })); -} - -export function getManPagesCountBySubcategory(mainCategory: string, subCategory: string): number { - const db = getDb(); - const stmt = db.prepare(` - SELECT COUNT(*) as count - FROM man_pages - WHERE main_category = ? AND sub_category = ? - `); - - const result = stmt.get(mainCategory, subCategory) as { count: number } | undefined; - return result?.count || 0; -} - -// Re-export types for convenience -export type { ManPage, Category, SubCategory } from '../../db/man_pages/man-pages-schema'; \ No newline at end of file diff --git a/frontend/src/lib/mcp-utils.ts b/frontend/src/lib/mcp-utils.ts index faa01c8979..c15b0065c3 100644 --- a/frontend/src/lib/mcp-utils.ts +++ b/frontend/src/lib/mcp-utils.ts @@ -130,3 +130,91 @@ export async function createCategoryRepositoryMap() { return categoryMap; } + +/** + * SSR: Get all MCP categories (for directory pagination) + */ +export async function getAllMcpCategories() { + const metadataEntries = await getCollection('mcpMetadata'); + const metadata = metadataEntries[0]?.data; + + if (!metadata) { + throw new Error('MCP metadata not found'); + } + + // Get all categories from metadata + const categories = Object.entries(metadata.categories).map( + ([id, categoryData]) => ({ + id, + name: categoryData.categoryDisplay, + description: '', + icon: id, + serverCount: categoryData.totalRepositories, + url: `/freedevtools/mcp/${id}/1/`, + }) + ); + + // Add descriptions from category data + const categoryEntries = await getCollection('mcpCategoryData'); + categoryEntries.forEach((entry) => { + const category = categories.find((c) => c.id === entry.data.category); + if (category) { + category.description = entry.data.description || ''; + } + }); + + return categories; +} + +/** + * SSR: Get all category IDs (for route validation) + */ +export async function getAllMcpCategoryIds(): Promise { + const categoryEntries = await getCollection('mcpCategoryData'); + return categoryEntries.map((entry) => entry.data.category); +} + +/** + * SSR: Get category data by ID + */ +export async function getMcpCategoryById(categoryId: string) { + const categoryEntries = await getCollection('mcpCategoryData'); + const entry = categoryEntries.find( + (e) => e.data.category === categoryId + ); + + if (!entry) { + return null; + } + + return { + category: entry.data.category, + categoryDisplay: entry.data.categoryDisplay, + description: entry.data.description || '', + repositories: entry.data.repositories, + }; +} + +/** + * SSR: Get repositories for a category (with pagination support) + */ +export async function getMcpCategoryRepositories(categoryId: string) { + const category = await getMcpCategoryById(categoryId); + if (!category) { + return []; + } + + // Include repository ID in each server object + return Object.entries(category.repositories).map(([repositoryId, server]) => ({ + ...server, + repositoryId: repositoryId, + })); +} + +/** + * SSR: Get MCP metadata + */ +export async function getMcpMetadata() { + const metadataEntries = await getCollection('mcpMetadata'); + return metadataEntries[0]?.data; +} diff --git a/frontend/src/lib/png-icons-utils.ts b/frontend/src/lib/png-icons-utils.ts deleted file mode 100644 index b873a7e0df..0000000000 --- a/frontend/src/lib/png-icons-utils.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { getCollection } from 'astro:content'; - -/** - * Generate paginated paths for PNG icons categories - */ -export async function generatePngIconsStaticPaths() { - const pngIconsEntries = await getCollection('pngIconsMetadata'); - const iconsData = pngIconsEntries[0]?.data; - - if (!iconsData) { - throw new Error('PNG icons metadata not found'); - } - - // Get all categories with their data - const categories = Object.entries(iconsData.clusters).map( - ([key, clusterData]) => ({ - id: key, - name: clusterData.name, - description: clusterData.description, - icon: `/freedevtools/png_icons/${clusterData.name}/`, - iconCount: clusterData.fileNames.length, - url: `/freedevtools/png_icons/${clusterData.name}/`, - keywords: clusterData.keywords, - features: clusterData.features, - fileNames: clusterData.fileNames, // Include fileNames for icon previews - }) - ); - - const itemsPerPage = 30; - const totalPages = Math.ceil(categories.length / itemsPerPage); - const paths: any[] = []; - - // Generate pagination pages (2, 3, 4, etc. - page 1 is handled by index.astro) - for (let i = 2; i <= totalPages; i++) { - paths.push({ - params: { page: i.toString() }, - props: { - type: 'pagination', - page: i, - itemsPerPage, - totalPages, - categories, - }, - }); - } - - return paths; -} - -/** - * Create a category-to-icons mapping for efficient lookups - */ -export async function createCategoryIconsMap() { - const pngIconsEntries = await getCollection('pngIconsMetadata'); - const iconsData = pngIconsEntries[0]?.data; - - if (!iconsData) { - throw new Error('PNG icons metadata not found'); - } - - const categoryMap = new Map(); - - for (const [key, clusterData] of Object.entries(iconsData.clusters)) { - const icons = clusterData.fileNames.map((fileObj: any) => { - const iconName = - typeof fileObj === 'string' - ? fileObj.replace('.svg', '') - : fileObj.fileName.replace('.svg', ''); - - return { - name: iconName, - url: `/freedevtools/png_icons/${clusterData.name}/${iconName}/`, - description: - typeof fileObj === 'object' ? fileObj.description || '' : '', - category: clusterData.name, - }; - }); - - categoryMap.set(clusterData.name, icons); - } - - return categoryMap; -} diff --git a/frontend/src/lib/tldr-constants.ts b/frontend/src/lib/tldr-constants.ts new file mode 100644 index 0000000000..1ef1d02f08 --- /dev/null +++ b/frontend/src/lib/tldr-constants.ts @@ -0,0 +1,153 @@ +export const emojiMap: Record = { + android: '📱', + aws: '☁️', + bash: '🐚', + common: '🔧', + git: '📦', + linux: '🐧', + macos: '🍎', + node: '🟢', + python: '🐍', + windows: '🪟', + docker: '🐳', + kubernetes: '☸️', + terraform: '🏗️', + npm: '📦', + yarn: '🧶', + pnpm: '📦', + go: '🐹', + rust: '🦀', + php: '🐘', + ruby: '💎', + java: '☕', + csharp: '🔷', + cpp: '⚡', + c: '⚡', + swift: '🦉', + kotlin: '🟣', + scala: '🔴', + clojure: '🟢', + haskell: '🔷', + ocaml: '🟠', + fsharp: '🔵', + elixir: '💜', + erlang: '🔴', + lua: '🔵', + perl: '🐪', + r: '📊', + matlab: '🧮', + julia: '🔴', + dart: '🎯', + flutter: '🦋', + react: '⚛️', + vue: '💚', + angular: '🅰️', + svelte: '🧡', + next: '▲', + nuxt: '💚', + gatsby: '🌐', + webpack: '📦', + vite: '⚡', + rollup: '📦', + parcel: '📦', + babel: '🔧', + typescript: '🔷', + javascript: '🟨', + html: '🌐', + css: '🎨', + sass: '💅', + less: '🔷', + stylus: '💄', + postcss: '🔧', + tailwind: '🎨', + bootstrap: '🎨', + material: '🎨', + antd: '🎨', + chakra: '🎨', + mantine: '🎨', + semantic: '🎨', + bulma: '🎨', + foundation: '🎨', + pure: '🎨', + skeleton: '🎨', + milligram: '🎨', + spectre: '🎨', + uikit: '🎨', + materialize: '🎨', + vuetify: '🎨', + quasar: '🎨', + prime: '🎨', + element: '🎨', + iview: '🎨', + naive: '🎨', + arco: '🎨', + tdesign: '🎨', + nutui: '🎨', + vant: '🎨', + mint: '🎨', + cube: '🎨', + mand: '🎨', + weui: '🎨', + jquery: '🎨', + lodash: '🔧', + moment: '📅', + dayjs: '📅', + 'date-fns': '📅', + luxon: '📅', + ramda: '🔧', + underscore: '🔧', + immutable: '🔧', + mobx: '🔧', + redux: '🔧', + vuex: '🔧', + pinia: '🔧', + zustand: '🔧', + jotai: '🔧', + recoil: '🔧', + swr: '🔧', + 'react-query': '🔧', + apollo: '🔧', + graphql: '🔧', + prisma: '🔧', + sequelize: '🔧', + mongoose: '🔧', + typeorm: '🔧', + knex: '🔧', + bookshelf: '🔧', + waterline: '🔧', + sails: '🔧', + loopback: '🔧', + feathers: '🔧', + hapi: '🔧', + fastify: '🔧', + koa: '🔧', + express: '🔧', + connect: '🔧', + restify: '🔧', + polka: '🔧', + micro: '🔧', + vercel: '🔧', + netlify: '🔧', + heroku: '🔧', + railway: '🔧', + render: '🔧', + fly: '🔧', + digitalocean: '☁️', + linode: '☁️', + vultr: '☁️', + azure: '☁️', + gcp: '☁️', + ibm: '☁️', + oracle: '☁️', + alibaba: '☁️', + tencent: '☁️', + baidu: '☁️', + huawei: '☁️', + rackspace: '☁️', + joyent: '☁️', + softlayer: '☁️', + ovh: '☁️', + scaleway: '☁️', + hetzner: '☁️', + contabo: '☁️', +}; diff --git a/frontend/src/lib/tldr-utils.ts b/frontend/src/lib/tldr-utils.ts deleted file mode 100644 index 62e73d5656..0000000000 --- a/frontend/src/lib/tldr-utils.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { getCollection } from 'astro:content'; - -/** - * Generate paginated paths for TLDR platforms - */ -export async function generateTldrStaticPaths() { - const tldrEntries = await getCollection('tldr'); - - const platformsByCount: Record = {}; - - for (const entry of tldrEntries) { - const pathParts = entry.id.split('/'); - const platform = pathParts[pathParts.length - 2]; - platformsByCount[platform] = (platformsByCount[platform] || 0) + 1; - } - - const platforms = Object.entries(platformsByCount).map(([name, count]) => ({ - name, - count, - url: `/freedevtools/tldr/${name}/`, - })); - - const itemsPerPage = 30; - const totalPages = Math.ceil(platforms.length / itemsPerPage); - const paths: any[] = []; - - // Generate pagination pages (2, 3, 4, etc. - page 1 is handled by index.astro) - for (let i = 2; i <= totalPages; i++) { - paths.push({ - params: { page: i.toString() }, - props: { - type: 'pagination', - page: i, - itemsPerPage, - totalPages, - platforms, - }, - }); - } - - return paths; -} - -/** - * Generate paginated paths for TLDR platform commands - */ -export async function generateTldrPlatformStaticPaths() { - const tldrEntries = await getCollection('tldr'); - - const platforms = new Set(); - for (const entry of tldrEntries) { - const pathParts = entry.id.split('/'); - const platform = pathParts[pathParts.length - 2]; - platforms.add(platform); - } - - const paths: any[] = []; - - for (const platform of platforms) { - const platformCommands = tldrEntries.filter((entry) => { - const pathParts = entry.id.split('/'); - const entryPlatform = pathParts[pathParts.length - 2]; - return entryPlatform === platform; - }); - - const itemsPerPage = 30; - const totalPages = Math.ceil(platformCommands.length / itemsPerPage); - - // Generate pagination pages for this platform (2, 3, 4, etc. - page 1 is handled by [platform]/index.astro) - for (let i = 2; i <= totalPages; i++) { - paths.push({ - params: { platform, page: i.toString() }, - props: { - type: 'pagination', - page: i, - itemsPerPage, - totalPages, - commands: platformCommands.map((entry) => { - const pathParts = entry.id.split('/'); - const fileName = pathParts[pathParts.length - 1]; - const commandName = fileName.replace('.md', ''); - - return { - name: entry.data.name || commandName, - url: - entry.data.path || - `/freedevtools/tldr/${platform}/${commandName}/`, - description: - entry.data.description || - `Documentation for ${commandName} command`, - category: entry.data.category, - }; - }), - }, - }); - } - } - - return paths; -} - -/** - * Get all TLDR platforms with their data - */ -export async function getAllTldrPlatforms() { - const tldrEntries = await getCollection('tldr'); - - const platformsByCount: Record = {}; - - for (const entry of tldrEntries) { - const pathParts = entry.id.split('/'); - const platform = pathParts[pathParts.length - 2]; - platformsByCount[platform] = (platformsByCount[platform] || 0) + 1; - } - - return Object.entries(platformsByCount).map(([name, count]) => ({ - name, - count, - url: `/freedevtools/tldr/${name}/`, - })); -} - -/** - * Get commands for a specific platform - */ -export async function getTldrPlatformCommands(platform: string) { - const tldrEntries = await getCollection('tldr'); - - return tldrEntries - .filter((entry) => { - const pathParts = entry.id.split('/'); - const entryPlatform = pathParts[pathParts.length - 2]; - return entryPlatform === platform; - }) - .map((entry) => { - const pathParts = entry.id.split('/'); - const fileName = pathParts[pathParts.length - 1]; - const commandName = fileName.replace('.md', ''); - - return { - name: entry.data.name || commandName, - url: - entry.data.path || `/freedevtools/tldr/${platform}/${commandName}/`, - description: - entry.data.description || `Documentation for ${commandName} command`, - category: entry.data.category, - }; - }); -} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index ac97cb0a07..7a829b43d2 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -9,10 +9,10 @@ export function formatNumber(num: number): string { if (num >= 1000) { return (num / 1000).toFixed(1) + 'k'; } - return num.toString(); + return num?.toString(); } -export function formatRepositoryName(name: string): string { +export function formatName(name: string): string { return name .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts new file mode 100644 index 0000000000..a47960692e --- /dev/null +++ b/frontend/src/middleware.ts @@ -0,0 +1,39 @@ +import { defineMiddleware, sequence } from 'astro:middleware'; +import { authMiddleware } from './components/auth/authMiddleware'; + +const ansiColors = { + reset: '\u001b[0m', + timestamp: '\u001b[35m', + green: '\u001b[32m', + yellow: '\u001b[33m', +} as const; + +const highlight = (text: string, color: string) => `${color}${text}${ansiColors.reset}`; + +// Logging middleware for svg_icons paths +const loggingMiddleware = defineMiddleware(async (context, next) => { + const pathname = context.url.pathname; + + if (pathname.startsWith('/freedevtools/svg_icons/')) { + const requestStart = Date.now(); + const timestampLabel = highlight(`[${new Date().toISOString()}]`, ansiColors.timestamp); + const requestLabel = highlight('Request reached server:', ansiColors.green); + console.log(`${timestampLabel} ${requestLabel} ${pathname}`); + + const handlerStart = Date.now(); + const response = await next(); + const handlerDuration = Date.now() - handlerStart; + + const requestDuration = Date.now() - requestStart; + const durationLabel = highlight('Total request time for', ansiColors.yellow); + const durationTimestamp = highlight(`[${new Date().toISOString()}]`, ansiColors.timestamp); + console.log(`${durationTimestamp} ${durationLabel} ${pathname}: ${requestDuration}ms`); + + return response; + } + + return next(); +}); + +// Chain middlewares: auth first, then logging +export const onRequest = sequence(authMiddleware, loggingMiddleware); diff --git a/frontend/src/pages/c/[category]/[name].astro b/frontend/src/pages/c/[category]/[name].astro deleted file mode 100644 index 34422f43c5..0000000000 --- a/frontend/src/pages/c/[category]/[name].astro +++ /dev/null @@ -1,193 +0,0 @@ ---- -import AdBanner from '../../../components/banner/AdBanner.astro'; -import Banner from '../../../components/banner/BannerIndex.astro'; -import CreditsButton from '../../../components/buttons/CreditsButton'; -import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; -import ToolContainer from '../../../components/tool/ToolContainer'; -import ToolHead from '../../../components/tool/ToolHead'; -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { getCheatsheet } from '../../../lib/cheatsheets-utils'; -import EABanner from '@/components/banner/EABanner.astro'; - -function processCheatsheetLinks(html: string): string { - // First, add IDs to all headings (h1-h6) so anchor links work - html = html.replace( - /<(h[1-6])([^>]*)>([^<]+)<\/h[1-6]>/gi, - (match, tag, attributes, content) => { - // Check if heading already has an ID - const existingIdMatch = attributes.match(/id="([^"]*)"/); - let anchorId = existingIdMatch ? existingIdMatch[1] : ''; - - // If no existing ID, generate one from content - if (!anchorId) { - anchorId = content - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') // Remove special chars - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Replace multiple hyphens with single - .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens - } - - // Add scroll margin to offset the header - use both class and inline style - const existingClass = attributes.includes('class=') ? attributes : ''; - const scrollMarginClass = existingClass - ? existingClass.replace(/class="([^"]*)"/, 'class="$1 scroll-mt-32"') - : 'class="scroll-mt-32"'; - - // Add inline style as backup to ensure scroll margin works - const existingStyle = attributes.includes('style=') ? attributes : ''; - const scrollMarginStyle = existingStyle - ? existingStyle.replace( - /style="([^"]*)"/, - 'style="$1; scroll-margin-top: 8rem;"' - ) - : 'style="scroll-margin-top: 8rem;"'; - - return `<${tag} ${scrollMarginClass} ${scrollMarginStyle} id="${anchorId}">${content}`; - } - ); - - // Then process the links - return html.replace( - /]*?)href="([^"]*?)"([^>]*?)>/g, - ( - match: string, - beforeHref: string, - href: string, - afterHref: string - ): string => { - // Keep absolute URLs (https/http) as clickable links - if (/^https?:\/\//i.test(href)) { - // Add target="_blank" for external links - if (!match.includes('target=')) { - return ``; - } - return match; - } - - // Handle anchor links (starting with #) - keep them as clickable for scrolling - if (href.startsWith('#')) { - return match; - } - - // Disable all relative links (/, ../, ./, etc.) by removing href and adding disabled styling - return ``; - } - ); -} - -export async function getStaticPaths() { - const sheetFiles = import.meta.glob('/data/cheatsheets/**/*.html', { - eager: true, - }); - const paths: Array<{ params: { category: string; name: string } }> = []; - - for (const path of Object.keys(sheetFiles)) { - const pathParts = path.split('/'); - const category = pathParts[pathParts.length - 2]; - const fileName = pathParts[pathParts.length - 1]; - const name = fileName.replace('.html', ''); - - paths.push({ - params: { category, name }, - }); - } - - return paths; -} - -const { category, name } = Astro.params; -const cheatsheetResult = await getCheatsheet(category!, name!); - -if (!cheatsheetResult) { - return Astro.redirect('/404'); -} - -const { content, metatags } = cheatsheetResult; - -// Process the content to handle links and add anchor IDs -const processedContent = processCheatsheetLinks(content); - -// Use metatags for title and description, with fallbacks -const title = metatags.title || name; -const description = metatags.description || `Cheatsheet for ${name}`; -const keywords = metatags.keywords || [ - 'free devtools', - 'cheatsheets', - category!, - name!, -]; - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Cheatsheets', href: '/freedevtools/c/' }, - { label: category, href: `/freedevtools/c/${category}/` }, - { label: name }, -]; ---- - - - - - -
- -
- - -
-
-
-
-
- - -
- -
- -
- - - diff --git a/frontend/src/pages/c/[category]/[page].astro b/frontend/src/pages/c/[category]/[page].astro deleted file mode 100644 index 4c23886aff..0000000000 --- a/frontend/src/pages/c/[category]/[page].astro +++ /dev/null @@ -1,103 +0,0 @@ ---- -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { - getAllCheatsheetCategories, - getCheatsheetsByCategory, -} from '../../../lib/cheatsheets-utils'; -import CategoryCheatsheetsPage from './_CategoryCheatsheetsPage.astro'; - -export async function getStaticPaths() { - const categories = await getAllCheatsheetCategories(); - const paths = []; - - for (const category of categories) { - const itemsPerPage = 30; - const totalPages = Math.ceil(category.cheatsheetCount / itemsPerPage); - - for (let page = 1; page <= totalPages; page++) { - paths.push({ - params: { - category: category.id, - page: page.toString(), - }, - props: { category: category.name }, - }); - } - } - - return paths; -} - -// Get category and page from params -const { category: categorySlug, page } = Astro.params; -const categoryName = - (Astro.props?.category as string) || - categorySlug! - .replace(/-/g, ' ') - .replace(/_/g, ' ') - .replace(/\b\w/g, (l: string) => l.toUpperCase()); -const currentPage = parseInt(page || '1'); - -// Get cheatsheets for this category -const cheatsheets = await getCheatsheetsByCategory(categoryName); -const totalCheatsheets = cheatsheets.length; - -// Pagination logic -const itemsPerPage = 30; -const totalPages = Math.ceil(totalCheatsheets / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedCheatsheets = cheatsheets.slice(startIndex, endIndex); - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Cheatsheets', href: '/freedevtools/c/' }, - { label: categoryName, href: `/freedevtools/c/${categorySlug}/` }, -]; - -// SEO data -const seoTitle = `${categoryName} Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = `Browse page ${currentPage} of ${categoryName.toLowerCase()} cheatsheets. Comprehensive reference covering commands, syntax, and key concepts.`; -const keywords = [ - categoryName.toLowerCase(), - 'cheatsheets', - 'reference', - 'commands', - 'syntax', - 'programming', - 'documentation', - 'page ' + currentPage, -]; ---- - - - - diff --git a/frontend/src/pages/c/[category]/[slug].astro b/frontend/src/pages/c/[category]/[slug].astro new file mode 100644 index 0000000000..4ec9ca6e1e --- /dev/null +++ b/frontend/src/pages/c/[category]/[slug].astro @@ -0,0 +1,312 @@ +--- +import AdBanner from '@/components/banner/AdBanner.astro'; +import Banner from '@/components/banner/BannerIndex.astro'; +import CreditsButton from '@/components/buttons/CreditsButton'; +import SeeAlsoIndex from '@/components/seealso/SeeAlsoIndex.astro'; +import ToolContainer from '@/components/tool/ToolContainer'; +import ToolHead from '@/components/tool/ToolHead'; +import BaseLayout from '@/layouts/BaseLayout.astro'; +import { + getCheatsheetByCategoryAndSlug, + getCheatsheetsByCategory, + getCategoryBySlug, +} from 'db/cheatsheets/cheatsheets-utils'; +import EABanner from '@/components/banner/EABanner.astro'; +import CategoryCheatsheetsPage from './_CategoryCheatsheetsPage.astro'; + +export const prerender = false; + +const { category: categorySlug, slug } = Astro.params; +const urlPath = Astro.url.pathname; + +if (!categorySlug || !slug) { + return new Response(null, { status: 404 }); +} + +// Redirect to add trailing slash if missing (BEFORE other checks) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +let cheatsheetData: any = null; +let paginationData: any = null; + +// Check if slug is numeric (pagination route) +if (/^\d+$/.test(slug)) { + // This is actually a pagination route for the category + const currentPage = parseInt(slug, 10); + + // Redirect /c/category/1 to /c/category + if (currentPage === 1) { + return Astro.redirect(`/freedevtools/c/${categorySlug}/`); + } + + // Validate category exists + const category = await getCategoryBySlug(categorySlug); + if (!category) { + return new Response(null, { status: 404 }); + } + + const categoryName = category.name; + + // Get cheatsheets for this category + const cheatsheets = await getCheatsheetsByCategory(categorySlug); + const totalCheatsheets = cheatsheets.length; + + // Pagination logic + const itemsPerPage = 30; + const totalPages = Math.ceil(totalCheatsheets / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedCheatsheets = cheatsheets.slice(startIndex, endIndex); + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + { label: categoryName, href: `/freedevtools/c/${categorySlug}/` }, + ]; + + // SEO data + const seoTitle = `${categoryName} Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of ${categoryName.toLowerCase()} cheatsheets. Comprehensive reference covering commands, syntax, and key concepts.`; + const keywords = [ + categoryName.toLowerCase(), + 'cheatsheets', + 'reference', + 'commands', + 'syntax', + 'programming', + 'documentation', + 'page ' + currentPage, + ]; + + paginationData = { + categoryName, + categorySlug, + paginatedCheatsheets: paginatedCheatsheets.map(c => ({ + name: c.slug, + url: `/freedevtools/c/${categorySlug}/${c.slug}/`, + description: c.description + })), + totalCheatsheets, + currentPage, + totalPages, + seoTitle, + seoDescription, + keywords, + breadcrumbItems, + }; +} else { + // This is a cheatsheet name (slug) + const cheatsheetResult = await getCheatsheetByCategoryAndSlug(categorySlug, slug); + + if (!cheatsheetResult) { + return new Response(null, { status: 404 }); + } + + const { content, title: metaTitle, description: metaDesc, keywords: metaKeywords } = cheatsheetResult; + + // Process the content to handle links and add anchor IDs + function processCheatsheetLinks(html: string): string { + // First, add IDs to all headings (h1-h6) so anchor links work + html = html.replace( + /<(h[1-6])([^>]*)>([^<]+)<\/h[1-6]>/gi, + (match, tag, attributes, content) => { + // Check if heading already has an ID + const existingIdMatch = attributes.match(/id="([^"]*)"/); + let anchorId = existingIdMatch ? existingIdMatch[1] : ''; + + // If no existing ID, generate one from content + if (!anchorId) { + anchorId = content + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') // Remove special chars + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single + .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens + } + + // Add scroll margin to offset the header - use both class and inline style + const existingClass = attributes.includes('class=') ? attributes : ''; + const scrollMarginClass = existingClass + ? existingClass.replace(/class="([^"]*)"/, 'class="$1 scroll-mt-32"') + : 'class="scroll-mt-32"'; + + // Add inline style as backup to ensure scroll margin works + const existingStyle = attributes.includes('style=') ? attributes : ''; + const scrollMarginStyle = existingStyle + ? existingStyle.replace( + /style="([^"]*)"/, + 'style="$1; scroll-margin-top: 8rem;"' + ) + : 'style="scroll-margin-top: 8rem;"'; + + return `<${tag} ${scrollMarginClass} ${scrollMarginStyle} id="${anchorId}">${content}`; + } + ); + + // Then process the links + return html.replace( + /]*?)href="([^"]*?)"([^>]*?)>/g, + ( + match: string, + beforeHref: string, + href: string, + afterHref: string + ): string => { + // Keep absolute URLs (https/http) as clickable links + if (/^https?:\/\//i.test(href)) { + // Add target="_blank" for external links + if (!match.includes('target=')) { + return ``; + } + return match; + } + + // Handle anchor links (starting with #) - keep them as clickable for scrolling + if (href.startsWith('#')) { + return match; + } + + // Disable all relative links (/, ../, ./, etc.) by removing href and adding disabled styling + return ``; + } + ); + } + + const processedContent = processCheatsheetLinks(content); + + // Use metatags for title and description, with fallbacks + const title = metaTitle || slug; + const description = metaDesc || `Cheatsheet for ${slug}`; + const keywords = metaKeywords || [ + 'free devtools', + 'cheatsheets', + categorySlug, + slug, + ]; + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + { label: categorySlug, href: `/freedevtools/c/${categorySlug}/` }, + { label: slug }, + ]; + + cheatsheetData = { + title, + description, + keywords, + processedContent, + categorySlug, + slug, + breadcrumbItems, + }; +} +--- + +{paginationData ? ( + + + +) : cheatsheetData ? ( + + + + +
+ +
+ + +
+
+
+
+
+ + +
+ {/* */} +
+ +
+ + + +) : null} + diff --git a/frontend/src/pages/c/[category]/_CategoryCheatsheetsPage.astro b/frontend/src/pages/c/[category]/_CategoryCheatsheetsPage.astro index 653d930605..d9e65140cb 100644 --- a/frontend/src/pages/c/[category]/_CategoryCheatsheetsPage.astro +++ b/frontend/src/pages/c/[category]/_CategoryCheatsheetsPage.astro @@ -1,9 +1,9 @@ --- -import Pagination from '../../../components/PaginationComponent.astro'; -import CreditsButton from '../../../components/buttons/CreditsButton'; -import ToolContainer from '../../../components/tool/ToolContainer'; -import ToolHead from '../../../components/tool/ToolHead'; -import AdBanner from '../../../components/banner/AdBanner.astro'; +import Pagination from '@/components/PaginationComponent.astro'; +import CreditsButton from '@/components/buttons/CreditsButton'; +import ToolContainer from '@/components/tool/ToolContainer'; +import ToolHead from '@/components/tool/ToolHead'; +import AdBanner from '@/components/banner/AdBanner.astro'; import EABanner from '@/components/banner/EABanner.astro'; const { categoryName, @@ -81,6 +81,7 @@ const { currentPage={currentPage} totalPages={totalPages} baseUrl={`/freedevtools/c/${categorySlug}/`} + alwaysIncludePageNumber={false} />
diff --git a/frontend/src/pages/c/[category]/index.astro b/frontend/src/pages/c/[category]/index.astro index 65aa215126..6442e22416 100644 --- a/frontend/src/pages/c/[category]/index.astro +++ b/frontend/src/pages/c/[category]/index.astro @@ -1,88 +1,210 @@ --- -import BaseLayout from '../../../layouts/BaseLayout.astro'; +import BaseLayout from '@/layouts/BaseLayout.astro'; import { - getAllCheatsheetCategories, + getAllCategories, getCheatsheetsByCategory, -} from '../../../lib/cheatsheets-utils'; + getTotalCategories, + getTotalCheatsheets, + getCategoryBySlug +} from 'db/cheatsheets/cheatsheets-utils'; import CategoryCheatsheetsPage from './_CategoryCheatsheetsPage.astro'; +import CheatsheetPage from '../_CheatsheetPage.astro'; -export async function getStaticPaths() { - const categories = await getAllCheatsheetCategories(); +export const prerender = false; +const { category: categorySlug } = Astro.params; +const urlPath = Astro.url.pathname; - return categories.map((category) => ({ - params: { category: category.id }, - props: { category: category.name }, - })); +if (!categorySlug) { + return new Response(null, { status: 404 }); } -// Get category from params -const { category: categorySlug } = Astro.params; -const categoryName = - (Astro.props?.category as string) || - categorySlug! - .replace(/-/g, ' ') - .replace(/_/g, ' ') - .replace(/\b\w/g, (l: string) => l.toUpperCase()); - -// Get cheatsheets for this category -const cheatsheets = await getCheatsheetsByCategory(categoryName); -const totalCheatsheets = cheatsheets.length; - -// Pagination logic for page 1 -const itemsPerPage = 30; -const currentPage = 1; -const totalPages = Math.ceil(totalCheatsheets / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedCheatsheets = cheatsheets.slice(startIndex, endIndex); - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Cheatsheets', href: '/freedevtools/c/' }, - { label: categoryName, href: `/freedevtools/c/${categorySlug}/` }, -]; - -// SEO data -const seoTitle = `${categoryName} Cheatsheets | Online Free DevTools by Hexmos`; -const seoDescription = `Comprehensive ${categoryName.toLowerCase()} cheatsheets covering commands, syntax, and key concepts for faster learning and recall.`; -const keywords = [ - categoryName.toLowerCase(), - 'cheatsheets', - 'reference', - 'commands', - 'syntax', - 'programming', - 'documentation', -]; +// Redirect to add trailing slash if missing (BEFORE other checks) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +let categoryData: any = null; +let paginationData: any = null; + +// If category is numeric, this is actually a pagination route +// Handle it directly (workaround for route priority) +if (/^\d+$/.test(categorySlug)) { + const currentPage = parseInt(categorySlug, 10); + + // Redirect /c/1 to /c + if (currentPage === 1) { + return Astro.redirect('/freedevtools/c/'); + } + + const itemsPerPage = 30; + const totalCategories = await getTotalCategories(); + const totalPages = Math.ceil(totalCategories / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + + // Calculate total cheatsheets + const totalCheatsheets = await getTotalCheatsheets(); + + // Get categories for current page + const categories = await getAllCategories(currentPage, itemsPerPage); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + ]; + + // SEO data + const seoTitle = `Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of our cheatsheets collection. Quick reference for commands, syntax, and programming concepts.`; + const canonical = `https://hexmos.com/freedevtools/c/${currentPage}/`; + + const mainKeywords = [ + 'cheatsheets', + 'reference', + 'commands', + 'syntax', + 'programming', + 'documentation', + 'quick reference', + 'command line', + 'cli', + 'terminal', + ]; + + paginationData = { + currentPage, + totalPages, + totalCategories, + totalCheatsheets, + categories, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + mainKeywords, + itemsPerPage, + }; +} else { + // Validate category exists + const category = await getCategoryBySlug(categorySlug); + + if (!category) { + return new Response(null, { status: 404 }); + } + + const categoryName = category.name; + + // Get cheatsheets for this category + // Note: getCheatsheetsByCategory returns all cheatsheets for the category sorted by slug + const cheatsheets = await getCheatsheetsByCategory(categorySlug); + const totalCheatsheets = cheatsheets.length; + + // Pagination logic for page 1 + const itemsPerPage = 30; + const currentPage = 1; + const totalPages = Math.ceil(totalCheatsheets / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedCheatsheets = cheatsheets.slice(startIndex, endIndex); + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + { label: categoryName, href: `/freedevtools/c/${categorySlug}/` }, + ]; + + // SEO data + const seoTitle = `${categoryName} Cheatsheets | Online Free DevTools by Hexmos`; + const seoDescription = `Comprehensive ${categoryName.toLowerCase()} cheatsheets covering commands, syntax, and key concepts for faster learning and recall.`; + const keywords = [ + categoryName.toLowerCase(), + 'cheatsheets', + 'reference', + 'commands', + 'syntax', + 'programming', + 'documentation', + ]; + + categoryData = { + categoryName, + categorySlug, + paginatedCheatsheets: paginatedCheatsheets.map(c => ({ + name: c.slug, // Using slug as name since we removed name column + url: `/freedevtools/c/${categorySlug}/${c.slug}/`, + description: c.description + })), + totalCheatsheets, + currentPage, + totalPages, + seoTitle, + seoDescription, + keywords, + breadcrumbItems, + }; +} --- - - - +{paginationData ? ( + + + +) : categoryData ? ( + + + +) : null} diff --git a/frontend/src/pages/c/[page].astro b/frontend/src/pages/c/[page].astro deleted file mode 100644 index 353c247381..0000000000 --- a/frontend/src/pages/c/[page].astro +++ /dev/null @@ -1,89 +0,0 @@ ---- -import BaseLayout from '../../layouts/BaseLayout.astro'; -import { generateCheatsheetStaticPaths, getAllCheatsheetCategories } from '../../lib/cheatsheets-utils'; -import CheatsheetPage from './_CheatsheetPage.astro'; - -export async function getStaticPaths() { - return await generateCheatsheetStaticPaths(); -} - -// Get page from params -const { page } = Astro.params; -const currentPage = parseInt(page || '1'); - -// Get all cheatsheet categories -const allCategories = await getAllCheatsheetCategories(); - -// Pagination logic -const itemsPerPage = 30; -const totalPages = Math.ceil(allCategories.length / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const categories = allCategories.slice(startIndex, endIndex); -const totalCategories = allCategories.length; - -// Calculate total cheatsheets -const totalCheatsheets = allCategories.reduce((total, category) => total + category.cheatsheetCount, 0); - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Cheatsheets', href: '/freedevtools/c/' } -]; - -// SEO data -const seoTitle = currentPage === 1 - ? "Cheatsheets - Quick Reference Commands & Syntax | Online Free DevTools by Hexmos" - : `Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; - -const seoDescription = currentPage === 1 - ? "Concise, easy-to-scan reference pages that summarize commands, syntax, and key concepts for faster learning and recall." - : `Browse page ${currentPage} of our cheatsheets collection. Quick reference for commands, syntax, and programming concepts.`; - -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/c/" - : `https://hexmos.com/freedevtools/c/${currentPage}/`; - -// Enhanced keywords for main page -const mainKeywords = [ - 'cheatsheets', - 'reference', - 'commands', - 'syntax', - 'programming', - 'documentation', - 'quick reference', - 'command line', - 'cli', - 'terminal' -]; ---- - - - - diff --git a/frontend/src/pages/c/_CheatsheetPage.astro b/frontend/src/pages/c/_CheatsheetPage.astro index 867557b62f..c10eeb7e7d 100644 --- a/frontend/src/pages/c/_CheatsheetPage.astro +++ b/frontend/src/pages/c/_CheatsheetPage.astro @@ -1,9 +1,9 @@ --- -import CreditsButton from '../../components/buttons/CreditsButton'; -import Pagination from '../../components/PaginationComponent.astro'; -import ToolContainer from '../../components/tool/ToolContainer'; -import ToolHead from '../../components/tool/ToolHead'; -import AdBanner from '../../components/banner/AdBanner.astro'; +import CreditsButton from '@/components/buttons/CreditsButton'; +import Pagination from '@/components/PaginationComponent.astro'; +import ToolContainer from '@/components/tool/ToolContainer'; +import ToolHead from '@/components/tool/ToolHead'; +import AdBanner from '@/components/banner/AdBanner.astro'; import EABanner from '@/components/banner/EABanner.astro'; const { categories, @@ -64,7 +64,7 @@ const {
{category.name.replace('-', ' ')} @@ -72,12 +72,12 @@ const {
- {category.cheatsheets.slice(0, 3).map((cheatsheet: any) => ( + {category.previewCheatsheets.map((cheatsheet: any) => ( - {cheatsheet.name} + {cheatsheet.slug} ))} {category.cheatsheetCount > 3 && ( @@ -89,7 +89,7 @@ const { - + \ No newline at end of file + diff --git a/frontend/src/pages/emojis/[category]/[page].astro b/frontend/src/pages/emojis/[category]/[page].astro index fc0d15feac..a4ab3f6a4c 100644 --- a/frontend/src/pages/emojis/[category]/[page].astro +++ b/frontend/src/pages/emojis/[category]/[page].astro @@ -4,129 +4,230 @@ import Pagination from '../../../components/PaginationComponent.astro'; import CreditsButton from '../../../components/buttons/CreditsButton'; import ToolContainer from '../../../components/tool/ToolContainer'; import ToolHead from '../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories } from '../../../lib/emojis'; +import { + getEmojisByCategoryPaginated, + getEmojiCategories, +} from 'db/emojis/emojis-utils'; import AdBanner from '../../../components/banner/AdBanner.astro'; +export const prerender = false; + const { category: categorySlug, page } = Astro.params; -const currentPage = parseInt(page || '1'); - -export async function getStaticPaths() { - const categories = getEmojiCategories(); - - const paths: any[] = []; - - for (const category of categories) { - if (category && category.trim() !== '') { - const categorySlug = category.toLowerCase().replace(/[^a-z0-9]+/g, '-'); - const emojis = await getEmojisByCategory(category); - const itemsPerPage = 30; - const totalPages = Math.ceil(emojis.length / itemsPerPage); - - // Generate pages for this category - for (let pageNum = 1; pageNum <= totalPages; pageNum++) { - paths.push({ - params: { - category: categorySlug, - page: pageNum.toString() - }, - props: { category } - }); - } - } - } - - return paths; +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Validate page is numeric +if (!page || !/^\d+$/.test(page)) { + return new Response(null, { status: 404 }); +} + +const currentPage = parseInt(page, 10); + +// Validate category exists +const allCategories = await getEmojiCategories(); +const categorySlugs = allCategories.map((cat) => + cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') +); + +if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); } -// Prefer original category from build-time props to preserve symbols like '&' -const categoryName = (Astro.props?.category as string) || categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); +// Find the actual category name from the slug +const categoryName = + allCategories.find( + (cat) => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug + ) || + categorySlug + .replace(/-/g, ' ') + .replace(/\b\w/g, (l: string) => l.toUpperCase()); // SEO metadata and descriptions for categories -const categorySeo: Record = { - 'Activities': { - title: 'Activities Emojis - Sports, Events, and Hobbies | Online Free DevTools by Hexmos', - description: 'Explore activities emojis covering sports, games, celebrations, and hobbies. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['activities emojis', 'sports emojis', 'games emojis', 'celebration emojis', 'hobby emojis', 'copy emoji', 'emoji meanings'] +const categorySeo: Record< + string, + { title: string; description: string; keywords: string[] } +> = { + Activities: { + title: + 'Activities Emojis - Sports, Events, and Hobbies | Online Free DevTools by Hexmos', + description: + 'Explore activities emojis covering sports, games, celebrations, and hobbies. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'activities emojis', + 'sports emojis', + 'games emojis', + 'celebration emojis', + 'hobby emojis', + 'copy emoji', + 'emoji meanings', + ], }, 'Animals & Nature': { - title: 'Animals & Nature Emojis - Wildlife, Plants, and Weather | Online Free DevTools by Hexmos', - description: 'Discover animals and nature emojis including wildlife, pets, plants, and weather symbols. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['animals emojis', 'nature emojis', 'wildlife emojis', 'plant emojis', 'weather emojis', 'copy emoji', 'emoji meanings'] + title: + 'Animals & Nature Emojis - Wildlife, Plants, and Weather | Online Free DevTools by Hexmos', + description: + 'Discover animals and nature emojis including wildlife, pets, plants, and weather symbols. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'animals emojis', + 'nature emojis', + 'wildlife emojis', + 'plant emojis', + 'weather emojis', + 'copy emoji', + 'emoji meanings', + ], }, 'Food & Drink': { - title: 'Food & Drink Emojis - Meals, Beverages, and Snacks | Online Free DevTools by Hexmos', - description: 'Browse food and drink emojis including meals, beverages, fruits, vegetables, and snacks. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['food emojis', 'drink emojis', 'meal emojis', 'beverage emojis', 'fruit emojis', 'copy emoji', 'emoji meanings'] + title: + 'Food & Drink Emojis - Meals, Beverages, and Snacks | Online Free DevTools by Hexmos', + description: + 'Browse food and drink emojis including meals, beverages, fruits, vegetables, and snacks. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'food emojis', + 'drink emojis', + 'meal emojis', + 'beverage emojis', + 'fruit emojis', + 'copy emoji', + 'emoji meanings', + ], }, - 'Objects': { - title: 'Objects Emojis - Technology, Tools, and Items | Online Free DevTools by Hexmos', - description: 'Explore object emojis including technology, tools, clothing, and everyday items. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['object emojis', 'technology emojis', 'tool emojis', 'clothing emojis', 'item emojis', 'copy emoji', 'emoji meanings'] + Objects: { + title: + 'Objects Emojis - Technology, Tools, and Items | Online Free DevTools by Hexmos', + description: + 'Explore object emojis including technology, tools, clothing, and everyday items. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'object emojis', + 'technology emojis', + 'tool emojis', + 'clothing emojis', + 'item emojis', + 'copy emoji', + 'emoji meanings', + ], }, 'People & Body': { - title: 'People & Body Emojis - Faces, Gestures, and Body Parts | Online Free DevTools by Hexmos', - description: 'Discover people and body emojis including faces, gestures, body parts, and family members. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['people emojis', 'body emojis', 'face emojis', 'gesture emojis', 'family emojis', 'copy emoji', 'emoji meanings'] + title: + 'People & Body Emojis - Faces, Gestures, and Body Parts | Online Free DevTools by Hexmos', + description: + 'Discover people and body emojis including faces, gestures, body parts, and family members. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'people emojis', + 'body emojis', + 'face emojis', + 'gesture emojis', + 'family emojis', + 'copy emoji', + 'emoji meanings', + ], }, 'Smileys & Emotion': { - title: 'Smileys & Emotion Emojis - Faces, Feelings, and Expressions | Online Free DevTools by Hexmos', - description: 'Browse smileys and emotion emojis including faces, feelings, and expressions. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['smiley emojis', 'emotion emojis', 'face emojis', 'feeling emojis', 'expression emojis', 'copy emoji', 'emoji meanings'] + title: + 'Smileys & Emotion Emojis - Faces, Feelings, and Expressions | Online Free DevTools by Hexmos', + description: + 'Browse smileys and emotion emojis including faces, feelings, and expressions. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'smiley emojis', + 'emotion emojis', + 'face emojis', + 'feeling emojis', + 'expression emojis', + 'copy emoji', + 'emoji meanings', + ], }, - 'Symbols': { - title: 'Symbols Emojis - Signs, Shapes, and Icons | Online Free DevTools by Hexmos', - description: 'Explore symbol emojis including signs, shapes, icons, and special characters. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['symbol emojis', 'sign emojis', 'shape emojis', 'icon emojis', 'character emojis', 'copy emoji', 'emoji meanings'] + Symbols: { + title: + 'Symbols Emojis - Signs, Shapes, and Icons | Online Free DevTools by Hexmos', + description: + 'Explore symbol emojis including signs, shapes, icons, and special characters. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'symbol emojis', + 'sign emojis', + 'shape emojis', + 'icon emojis', + 'character emojis', + 'copy emoji', + 'emoji meanings', + ], }, 'Travel & Places': { - title: 'Travel & Places Emojis - Destinations, Transportation, and Locations | Online Free DevTools by Hexmos', - description: 'Discover travel and places emojis including destinations, transportation, and location symbols. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['travel emojis', 'places emojis', 'destination emojis', 'transportation emojis', 'location emojis', 'copy emoji', 'emoji meanings'] + title: + 'Travel & Places Emojis - Destinations, Transportation, and Locations | Online Free DevTools by Hexmos', + description: + 'Discover travel and places emojis including destinations, transportation, and location symbols. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'travel emojis', + 'places emojis', + 'destination emojis', + 'transportation emojis', + 'location emojis', + 'copy emoji', + 'emoji meanings', + ], + }, + Flags: { + title: + 'Flags Emojis - Country and Regional Flags | Online Free DevTools by Hexmos', + description: + 'Browse flag emojis including country flags, regional flags, and special flags. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: [ + 'flag emojis', + 'country emojis', + 'regional emojis', + 'national emojis', + 'copy emoji', + 'emoji meanings', + ], }, - 'Flags': { - title: 'Flags Emojis - Country and Regional Flags | Online Free DevTools by Hexmos', - description: 'Browse flag emojis including country flags, regional flags, and special flags. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['flag emojis', 'country emojis', 'regional emojis', 'national emojis', 'copy emoji', 'emoji meanings'] - } }; const seoData = categorySeo[categoryName] || { title: `${categoryName} Emojis | Online Free DevTools by Hexmos`, description: `Explore ${categoryName.toLowerCase()} emojis. Copy emoji, view meanings, and find shortcodes instantly.`, - keywords: [`${categoryName.toLowerCase()} emojis`, 'copy emoji', 'emoji meanings', 'emoji shortcodes'] + keywords: [ + `${categoryName.toLowerCase()} emojis`, + 'copy emoji', + 'emoji meanings', + 'emoji shortcodes', + ], }; -// Get emojis for this category -const emojis = await getEmojisByCategory(categoryName); -const totalEmojis = emojis.length; - -// Pagination logic +// Get emojis for this category (paginated query) const itemsPerPage = 36; +const { emojis: paginatedEmojis, total: totalEmojis } = + await getEmojisByCategoryPaginated(categoryName, currentPage, itemsPerPage); const totalPages = Math.ceil(totalEmojis / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedEmojis = emojis.slice(startIndex, endIndex); // Breadcrumb items const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` } + { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` }, ]; --- - @@ -143,7 +244,9 @@ const breadcrumbItems = [
-
{totalEmojis.toLocaleString()}
+
+ {totalEmojis.toLocaleString()} +
Emojis
@@ -156,41 +259,71 @@ const breadcrumbItems = [
- Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page {currentPage} of {totalPages}) + Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page { + currentPage + } of {totalPages})
-
- {paginatedEmojis.map((emoji) => ( - -
-
{emoji.code || emoji.fluentui_metadata?.glyph || (emoji as any).glyph || ''}
-
- {emoji.title || emoji.fluentui_metadata?.cldr || emoji.slug} -
-
-
- ))} + - -
+
@@ -210,12 +343,12 @@ const breadcrumbItems = [ // Copy emoji functionality // document.addEventListener('DOMContentLoaded', function() { // const emojiCards = document.querySelectorAll('.emoji-card'); - + // emojiCards.forEach((card: any) => { // card.addEventListener('click', function(this: any) { // const emoji = this.dataset.emoji; // const title = this.dataset.title; - + // // Copy to clipboard // navigator.clipboard.writeText(emoji).then(() => { // // Show feedback @@ -223,7 +356,7 @@ const breadcrumbItems = [ // this.querySelector('.text-xs').textContent = 'Copied!'; // this.style.backgroundColor = '#10b981'; // this.style.color = 'white'; - + // setTimeout(() => { // this.querySelector('.text-xs').textContent = originalText; // this.style.backgroundColor = ''; @@ -234,5 +367,5 @@ const breadcrumbItems = [ // }); // }); // }); - // }); + // }); diff --git a/frontend/src/pages/emojis/[page].astro b/frontend/src/pages/emojis/[page].astro deleted file mode 100644 index 33386ff80c..0000000000 --- a/frontend/src/pages/emojis/[page].astro +++ /dev/null @@ -1,229 +0,0 @@ ---- -import CreditsButton from '../../components/buttons/CreditsButton'; -import Pagination from '../../components/PaginationComponent.astro'; -import BaseLayout from '../../layouts/BaseLayout.astro'; -import ToolContainer from '../../components/tool/ToolContainer'; -import ToolHead from '../../components/tool/ToolHead'; -import { getAllEmojis, getEmojiCategories } from '../../lib/emojis'; -import AdBanner from '../../components/banner/AdBanner.astro'; - -export async function getStaticPaths() { - const emojis = await getAllEmojis(); - const categories = getEmojiCategories(); - - const itemsPerPage = 30; - const totalPages = Math.ceil(categories.length / itemsPerPage); - - const paths = []; - for (let page = 1; page <= totalPages; page++) { - paths.push({ - params: { page: page.toString() }, - }); - } - - return paths; -} - -const { page } = Astro.params; -const currentPage = parseInt(page || '1'); - -const emojis = await getAllEmojis(); - -// Metadata-based categories -const categories = getEmojiCategories(); -const emojisByCategory: Record = {}; -for (const cat of categories) { - emojisByCategory[cat] = emojis.filter( - (e) => - (e.fluentui_metadata?.group || - e.emoji_net_data?.category || - (e as any).given_category || - 'Other') === cat - ); -} - -// Sort categories and emojis within categories -const sortedCategories = Object.keys(emojisByCategory).sort(); -for (const category of sortedCategories) { - emojisByCategory[category].sort((a, b) => { - const titleA = a.title || a.fluentui_metadata?.cldr || a.slug || ''; - const titleB = b.title || b.fluentui_metadata?.cldr || b.slug || ''; - return titleA.localeCompare(titleB); - }); -} - -// Pagination logic -const itemsPerPage = 36; -const totalPages = Math.ceil(sortedCategories.length / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedCategories = sortedCategories.slice(startIndex, endIndex); - -// Category icons mapping -const categoryIconMap: Record = { - 'Smileys & Emotion': '😀', - 'People & Body': '👤', - 'Animals & Nature': '🐶', - 'Food & Drink': '🍎', - 'Travel & Places': '✈️', - Activities: '⚽', - Objects: '📱', - Symbols: '❤️', - Flags: '🏁', - Other: '❓', -}; - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, -]; ---- - - - -
- -
- - - -
-
-
-
- {sortedCategories.length} -
-
Categories
-
-
-
- {emojis.length.toLocaleString()} -
-
Emojis
-
-
-
- -
- { - paginatedCategories.map((category) => ( -
-
-
- - {categoryIconMap[category] || '❓'} - -
- - {category.replace('-', ' ')} - -
- -

- {emojisByCategory[category].length} emojis available -

- -
- {emojisByCategory[category].slice(0, 5).map((emoji) => { - const emojiName = - emoji.title || emoji.fluentui_metadata?.cldr || emoji.slug; - const truncatedName = - emojiName.length > 27 - ? emojiName.substring(0, 27) + '...' - : emojiName; - return ( - - {emoji.code || emoji.emoji} {truncatedName} - - ); - })} - {emojisByCategory[category].length > 5 && ( -

- +{emojisByCategory[category].length - 5} more items -

- )} -
- - -
- )) - } -
- - - - - - -
-
- - diff --git a/frontend/src/pages/emojis/[slug].astro b/frontend/src/pages/emojis/[slug].astro deleted file mode 100644 index 26dbe27ba5..0000000000 --- a/frontend/src/pages/emojis/[slug].astro +++ /dev/null @@ -1,137 +0,0 @@ ---- -import AdBanner from '../../components/banner/AdBanner.astro'; -import CreditsButton from '../../components/buttons/CreditsButton'; -import ToolContainer from '../../components/tool/ToolContainer'; -import ToolHead from '../../components/tool/ToolHead'; -import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getEmojiBySlug, getEmojiImages } from '../../lib/emojis'; -import { apple_vendor_excluded_emojis, discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; -import EachEmojiPage from './EachEmojiPage.astro'; - -export async function getStaticPaths() { - const { getAllEmojis } = await import('../../lib/emojis'); - const emojis = getAllEmojis(); - - return emojis - .filter((emoji) => emoji.slug && emoji.slug.trim() !== '') - .map((emoji) => ({ - params: { slug: emoji.slug }, - props: { emoji }, - })); -} - -const { slug } = Astro.params; -const emoji = getEmojiBySlug(slug!); -const images = getEmojiImages(slug!); - -if (!emoji) { - return Astro.redirect('/freedevtools/emojis/'); -} - -const cleanDescription = (text?: string) => { - if (!text) return ''; - return text - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/[?]{2,}/g, '') - .trim(); -}; - -// Get category for breadcrumb -const categoryName = (emoji.category || 'Other') as string; - -const categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9]+/g, '-'); - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` }, - { label: emoji.title || emoji.slug }, -]; ---- - - - -
- -
- - - - - -
-
- - ← Back to Emojis - - { - emoji.category ? ( - - View{' '} - {emoji.category - .replace(/-/g, ' ') - .replace(/\b\w/g, (l) => l.toUpperCase())}{' '} - Category - - ) : null - } - - { - emoji.apple_vendor_description && - !apple_vendor_excluded_emojis.includes(emoji.slug) ? ( - - 🍎 View Apple Version - - ) : null - } - - { - emoji.discord_vendor_description && - !discord_vendor_excluded_emojis.includes(emoji.slug) ? ( - - 🎮 View Discord Version - - ) : null - } - - - -
-
-
-
diff --git a/frontend/src/pages/emojis/_EmojiPage.astro b/frontend/src/pages/emojis/_EmojiPage.astro index 97bae6cf9d..c066a504b7 100644 --- a/frontend/src/pages/emojis/_EmojiPage.astro +++ b/frontend/src/pages/emojis/_EmojiPage.astro @@ -4,15 +4,16 @@ import CreditsButton from '../../components/buttons/CreditsButton'; import Pagination from '../../components/PaginationComponent.astro'; import ToolContainer from '../../components/tool/ToolContainer'; import ToolHead from '../../components/tool/ToolHead'; -import { categoryIconMap } from "../../lib/emojis" -const { - categories, - emojisByCategory, - currentPage, - totalPages, - totalCategories, - totalEmojis, - breadcrumbItems +import { categoryIconMap } from 'db/emojis/emojis-utils'; +const { + categories, + emojisByCategory, + categoryCounts = {}, + currentPage, + totalPages, + totalCategories, + totalEmojis, + breadcrumbItems, } = Astro.props; --- @@ -34,7 +35,9 @@ const {
Categories
-
{totalEmojis.toLocaleString()}
+
+ {totalEmojis.toLocaleString()} +
Emojis
@@ -43,84 +46,108 @@ const {
- Showing {categories.length} of {totalCategories} categories (Page {currentPage} of {totalPages}) + Showing {categories.length} of {totalCategories} categories (Page { + currentPage + } of {totalPages})
-
- {categories.map((category: any) => { - const emojis = emojisByCategory[category]; +
+ { + categories.map((category: any) => { + const emojis = emojisByCategory[category]; - // Hide "Other" category if it has no emojis - if (category === "Other" && (!emojis || emojis.length === 0)) return null; - return ( -
-
-
- - {categoryIconMap[category] || "❓"} - -
- - {category.replace('-', ' ')} - -
- -

- {emojisByCategory[category].length} emojis available -

- -
- {emojisByCategory[category].slice(0, 5).map((emoji: any) => { - const emojiName = emoji.title || emoji.fluentui_metadata?.cldr || emoji.slug; - const truncatedName = emojiName.length > 27 ? emojiName.substring(0, 27) + '...' : emojiName; - return ( - +
+
+ + {categoryIconMap[category] || '❓'} + +
+
- {emoji.code || emoji.emoji} {truncatedName} + {category.replace('-', ' ')} - ); - })} - {emojisByCategory[category].length > 5 && ( -

- +{emojisByCategory[category].length - 5} more items +

+ +

+ {( + categoryCounts[category] || emojisByCategory[category].length + ).toLocaleString()}{' '} + emojis available

- )} -
- - -
- )})} + +
+ {emojisByCategory[category].slice(0, 5).map((emoji: any) => { + const emojiName = + emoji.title || emoji.fluentui_metadata?.cldr || emoji.slug; + const truncatedName = + emojiName.length > 27 + ? emojiName.substring(0, 27) + '...' + : emojiName; + return ( + + {emoji.code || emoji.emoji} {truncatedName} + + ); + })} + {(categoryCounts[category] || emojisByCategory[category].length) > + 5 && ( +

+ + + {( + (categoryCounts[category] || + emojisByCategory[category].length) - 5 + ).toLocaleString()}{' '} + more items +

+ )} +
+ + +
+ ); + }) + }
- +
-

Vendors

+

+ Vendors +

Explore emoji designs by platform vendors

-
diff --git a/frontend/src/pages/emojis/apple-emojis/[category].astro b/frontend/src/pages/emojis/apple-emojis/[category].astro index 28b6aff92f..aa3d2aa6fb 100644 --- a/frontend/src/pages/emojis/apple-emojis/[category].astro +++ b/frontend/src/pages/emojis/apple-emojis/[category].astro @@ -5,37 +5,76 @@ import Pagination from '../../../components/PaginationComponent.astro'; import CreditsButton from '../../../components/buttons/CreditsButton'; import ToolContainer from '../../../components/tool/ToolContainer'; import ToolHead from '../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage } from '../../../lib/emojis'; - -// Generate static paths -export async function getStaticPaths() { - const categories = getEmojiCategories(); - return categories - .filter((cat) => cat && cat.trim() !== '') - .map((cat) => ({ - params: { category: cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') }, - props: { category: cat }, - })); -} +import { getEmojiCategories, getEmojiBySlug, getAppleEmojiBySlug, getEmojisByCategoryWithAppleImagesPaginated } from 'db/emojis/emojis-utils'; +import VendorEmojiPage from '@/components/VendorEmojiPage'; +import { discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; + +export const prerender = false; const { category: categorySlug } = Astro.params; -const categoryName = - (Astro.props?.category as string) || - categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); - -// const emojis = await getEmojisByCategory(categoryName, "apple"); -const emojis = (await getEmojisByCategory(categoryName, "apple")) - .map(e => ({ - ...e, - latestAppleImage: fetchLatestAppleImage(e.slug) - })); - -if (emojis.length === 0) { - return Astro.redirect('/freedevtools/emojis/apple-emojis/'); +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Get all categories to validate +const allCategories = await getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); + +// Check if category is numeric (pagination route) - 404 since there's no pagination on apple-emojis index +if (categorySlug && /^\d+$/.test(categorySlug)) { + return new Response(null, { status: 404 }); } -// --- Apple-specific category descriptions --- -export const appleCategoryDescriptions = { +let emojiData: any = null; +let categoryData: any = null; + +// Check if this is actually an emoji slug (not a category) +// Since [category].astro matches first, we need to handle emoji slugs here +const emoji = await getEmojiBySlug(categorySlug!); +if (emoji && !categorySlugs.includes(categorySlug!)) { + // This is an emoji slug, not a category - handle it here + const appleEmoji = await getAppleEmojiBySlug(categorySlug!); + + if (!appleEmoji) { + return new Response(null, { status: 404 }); + } + + const cleanDescription = (text?: string) => { + if (!text) return ''; + return text + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/[?]{2,}/g, '') + .trim(); + }; + + const cleanedDescription = cleanDescription(appleEmoji.apple_vendor_description || appleEmoji.description); + + emojiData = { + emoji: appleEmoji, + cleanedDescription, + }; +} else { + // Validate category exists + if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); + } + + // Find the actual category name from the slug + const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug + ) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); + + // --- Apple-specific category descriptions --- + const appleCategoryDescriptions: Record = { "Smileys & Emotion": "Apple's Smileys feature glossy shading and expressive faces that set the tone for iMessage and social platforms. The distinctive rounded eyes and vibrant gradients are signature Apple touches.", "People & Body": "Apple leads on inclusivity—supporting diverse skin tones, gender options, and custom Memoji. People and gestures have soft edges and subtle shadows, making them feel inviting and lively on iOS.", "Animals & Nature": "Animals in Apple's emoji set are playful and detailed, often with friendly eyes and vivid colors. Nature motifs leverage semi-realistic illustrations that feel right at home in iOS dark and light mode.", @@ -45,57 +84,132 @@ export const appleCategoryDescriptions = { "Objects": "Apple renders everyday objects with photorealistic vibes—from high-res tech gadgets to lifelike accessories—ensuring each emoji looks detailed and familiar across Apple devices.", "Symbols": "Apple's symbols blend clarity with style. Glassy gradients, subtle depth, and clean shapes set apart icons like hearts, arrows, and warning signs from standard flat glyphs.", "Flags": "Apple flags maintain accurate proportions and vibrant colors for easy recognition, with an extra emphasis on clarity and accessibility for global users.", - "Other": "Apple's unique emojis in the 'Other' category often represent novelty, tech, and recent trends, all interpreted with its signature visual polish and device-optimized detail." -}; - -// --- Pagination setup --- -const totalEmojis = emojis.length; -const itemsPerPage = 36; -const currentPage = 1; -const totalPages = Math.ceil(totalEmojis / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedEmojis = emojis.slice(startIndex, endIndex); - -// --- Breadcrumbs --- -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: 'Apple Emojis', href: '/freedevtools/emojis/apple-emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/apple-emojis/${categorySlug}/` } -]; + "Other": "Apple's unique emojis in the 'Other' category often represent novelty, tech, and recent trends, all interpreted with its signature visual polish and device-optimized detail." + }; + + // --- Pagination setup (optimized query) --- + const itemsPerPage = 36; + const currentPage = 1; + const { emojis: paginatedEmojis, total: totalEmojis } = await getEmojisByCategoryWithAppleImagesPaginated(categoryName, currentPage, itemsPerPage); + const totalPages = Math.ceil(totalEmojis / itemsPerPage); + + if (paginatedEmojis.length === 0) { + return Astro.redirect('/freedevtools/emojis/apple-emojis/'); + } + + // --- Breadcrumbs --- + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + { label: 'Apple Emojis', href: '/freedevtools/emojis/apple-emojis/' }, + { label: categoryName, href: `/freedevtools/emojis/apple-emojis/${categorySlug}/` } + ]; + + categoryData = { + categoryName, + categorySlug, + totalEmojis, + itemsPerPage, + currentPage, + totalPages, + paginatedEmojis, + breadcrumbItems, + appleCategoryDescriptions, + }; +} --- - +{emojiData ? ( + +
+
+ +
+
+ +
+ + + + +
+
+ + ← Back to Apple Emojis + + + {emojiData.emoji.category ? ( + + View {emojiData.emoji.category.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())} Category + + ) : null} + + {emojiData.emoji.discord_vendor_description && + !discord_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( + + 🎮 View Discord Version + + ) : null} +
+
+
+
+) : categoryData ? ( +
-
{totalEmojis.toLocaleString()}
+
{categoryData.totalEmojis.toLocaleString()}
Emojis
-
{totalPages}
+
{categoryData.totalPages}
Pages
@@ -104,13 +218,13 @@ const breadcrumbItems = [
- Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page {currentPage} of {totalPages}) + Showing {categoryData.paginatedEmojis.length} of {categoryData.totalEmojis} emojis (Page {categoryData.currentPage} of {categoryData.totalPages})
- {paginatedEmojis.map((emoji) => { + {categoryData.paginatedEmojis.map((emoji) => { const emojiChar = emoji.code || ''; const graphemeCount = [...emojiChar].length; const zwjCount = (emojiChar.match(/\u200d/g) || []).length; @@ -152,9 +266,9 @@ const breadcrumbItems = [ @@ -171,6 +285,7 @@ const breadcrumbItems = [
+) : null} diff --git a/frontend/src/pages/emojis/apple-emojis/[slug].astro b/frontend/src/pages/emojis/apple-emojis/[slug].astro deleted file mode 100644 index fceae61804..0000000000 --- a/frontend/src/pages/emojis/apple-emojis/[slug].astro +++ /dev/null @@ -1,102 +0,0 @@ ---- -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import AdBanner from '../../../components/banner/AdBanner'; -import { getAllAppleEmojis,getAppleEmojiBySlug } from '@/lib/emojis'; -import VendorEmojiPage from '@/components/VendorEmojiPage'; -import { discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; - -export async function getStaticPaths() { - const emojis = getAllAppleEmojis(); - return emojis.map((emoji) => ({ - params: { slug: emoji.slug }, - props: { emoji } - })); -} - -const { slug } = Astro.params; -const emoji = getAppleEmojiBySlug(slug!); - -if (!emoji) { - return Astro.redirect('/freedevtools/emojis/apple-emojis/'); -} - -const cleanDescription = (text?: string) => { - if (!text) return ''; - return text - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/[?]{2,}/g, '') - .trim(); -}; - - ---- - - -
-
- -
-
- -
- - - - -
-
- - ← Back to Apple Emojis - - - { - emoji.category ? ( - - View {emoji.category.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())} Category - - ) : null - } - - { - emoji.discord_vendor_description && - !discord_vendor_excluded_emojis.includes(emoji.slug) ? ( - - 🎮 View Discord Version - - ) : null - } -
-
-
-
diff --git a/frontend/src/pages/emojis/apple-emojis/index.astro b/frontend/src/pages/emojis/apple-emojis/index.astro index a77fd607f5..bfee6d8be4 100644 --- a/frontend/src/pages/emojis/apple-emojis/index.astro +++ b/frontend/src/pages/emojis/apple-emojis/index.astro @@ -1,34 +1,38 @@ --- -import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { + fetchImageFromDB, + getAppleCategoriesWithPreviewEmojis, + getEmojiCategories, + type EmojiData, +} from 'db/emojis/emojis-utils'; import AdBanner from '../../../components/banner/AdBanner'; -import { getAllEmojis, getEmojiCategories, fetchImageFromDB } from '../../../lib/emojis'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; -// Load all emojis -const emojis = await getAllEmojis(); +// Get categories with preview emojis in a single optimized query +const categoriesWithPreview = await getAppleCategoriesWithPreviewEmojis(5); +const categories = await getEmojiCategories(); -// Metadata-based categories -const categories = getEmojiCategories(); +// Transform to match expected format const emojisByCategory: Record = {}; - -// Group emojis by category -for (const cat of categories) { - emojisByCategory[cat] = emojis.filter((e) => (e.category || 'Other') === cat); +const categoryCounts: Record = {}; +for (const catWithPreview of categoriesWithPreview) { + emojisByCategory[catWithPreview.category] = catWithPreview.previewEmojis.map( + (e) => + ({ + code: e.code, + slug: e.slug, + title: e.title, + }) as EmojiData + ); + categoryCounts[catWithPreview.category] = catWithPreview.count; } -// Sort categories alphabetically and exclude "Other" -const sortedCategories = Object.keys(emojisByCategory) +// Sort categories alphabetically (already sorted from query, but ensure) +const sortedCategories = categoriesWithPreview + .map((c) => c.category) .filter((cat) => cat !== 'Other') .sort(); -// Sort emojis within each category by title (fallback to slug) -for (const category of sortedCategories) { - emojisByCategory[category].sort((a, b) => { - const titleA = a.title || a.slug || ''; - const titleB = b.title || b.slug || ''; - return titleA.localeCompare(titleB); - }); -} - // Pagination logic for page 1 const itemsPerPage = 30; const currentPage = 1; @@ -42,21 +46,24 @@ const totalCategories = sortedCategories.length; const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: 'Apple Emojis' } + { label: 'Apple Emojis' }, ]; // SEO data -const seoTitle = currentPage === 1 - ? "Apple Emojis Reference - Browse & Copy Apple Emojis | Online Free DevTools by Hexmos" - : `Apple Emojis Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; +const seoTitle = + currentPage === 1 + ? 'Apple Emojis Reference - Browse & Copy Apple Emojis | Online Free DevTools by Hexmos' + : `Apple Emojis Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = currentPage === 1 - ? "Browse Apple's version of emojis by category. Copy instantly, explore meanings, and discover platform-specific emoji designs." - : `Browse page ${currentPage} of Apple's emoji reference. Copy instantly, explore meanings, and discover iOS-style emoji artwork.`; +const seoDescription = + currentPage === 1 + ? "Browse Apple's version of emojis by category. Copy instantly, explore meanings, and discover platform-specific emoji designs." + : `Browse page ${currentPage} of Apple's emoji reference. Copy instantly, explore meanings, and discover iOS-style emoji artwork.`; -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/emojis/apple-emojis/" - : `https://hexmos.com/freedevtools/emojis/apple-emojis/${currentPage}/`; +const canonical = + currentPage === 1 + ? 'https://hexmos.com/freedevtools/emojis/apple-emojis/' + : `https://hexmos.com/freedevtools/emojis/apple-emojis/${currentPage}/`; const mainKeywords = [ 'apple emojis', @@ -68,28 +75,44 @@ const mainKeywords = [ 'emoji meanings', 'emoji shortcodes', 'emoji library', - 'emoji copy' + 'emoji copy', ]; // Category icons -export const categoryIconMap: Record = { - "Smileys & Emotion": fetchImageFromDB("slightly-smiling-face", "slightly-smiling-face_iOS_18.4.png")!, - "People & Body": fetchImageFromDB("bust-in-silhouette", "bust-in-silhouette_1f464_iOS_18.4.png")!, - "Animals & Nature": fetchImageFromDB("dog-face", "dog-face_1f436_iOS_18.4.png")!, - "Food & Drink": fetchImageFromDB("red-apple", "red-apple_1f34e_iOS_18.4.png")!, - "Travel & Places": fetchImageFromDB("airplane", "airplane_iOS_18.4.png")!, - "Activities": fetchImageFromDB("soccer-ball", "soccer-ball_26bd_iOS_18.4.png")!, - "Objects": fetchImageFromDB("mobile-phone", "mobile-phone_iOS_18.4.png")!, - "Symbols": fetchImageFromDB("check-mark-button", "check-mark-button_2705_iOS_18.4.png")!, - "Flags": fetchImageFromDB("chequered-flag", "chequered-flag_iOS_18.4.png")!, - "Other": fetchImageFromDB("question-mark", "question-mark_2753_iOS_18.4.png")!, +const categoryIconMap: Record = { + 'Smileys & Emotion': (await fetchImageFromDB( + 'slightly-smiling-face', + 'slightly-smiling-face_iOS_18.4.png' + )) || '', + 'People & Body': (await fetchImageFromDB( + 'bust-in-silhouette', + 'bust-in-silhouette_1f464_iOS_18.4.png' + )) || '', + 'Animals & Nature': (await fetchImageFromDB( + 'dog-face', + 'dog-face_1f436_iOS_18.4.png' + )) || '', + 'Food & Drink': (await fetchImageFromDB( + 'red-apple', + 'red-apple_1f34e_iOS_18.4.png' + )) || '', + 'Travel & Places': (await fetchImageFromDB('airplane', 'airplane_iOS_18.4.png')) || '', + Activities: (await fetchImageFromDB('soccer-ball', 'soccer-ball_26bd_iOS_18.4.png')) || '', + Objects: (await fetchImageFromDB('mobile-phone', 'mobile-phone_iOS_18.4.png')) || '', + Symbols: (await fetchImageFromDB( + 'check-mark-button', + 'check-mark-button_2705_iOS_18.4.png' + )) || '', + Flags: (await fetchImageFromDB('chequered-flag', 'chequered-flag_iOS_18.4.png')) || '', + Other: (await fetchImageFromDB('question-mark', 'question-mark_2753_iOS_18.4.png')) || '', }; - --- - = { partOf="Free DevTools" partOfUrl="https://hexmos.com/freedevtools/" keywords={mainKeywords} - features={["Apple-style designs", "Browse by category", "Copy instantly", "Compare with Unicode", "Free access"]} + features={[ + 'Apple-style designs', + 'Browse by category', + 'Copy instantly', + 'Compare with Unicode', + 'Free access', + ]} emojiCategory="Apple Emojis" >
@@ -113,16 +142,23 @@ export const categoryIconMap: Record = {

@@ -130,56 +166,111 @@ export const categoryIconMap: Record = {

- Apple's emojis are known for their vibrant, playful designs and the frequent updates that come with each iOS release. - Instead of relying on standard Unicode glyphs, Apple creates custom artwork for every emoji, which makes their style instantly recognizable to iPhone, iPad, and Mac users. - Discover Apple's full emoji collection here, complete with previews and instant copy options for every category. - Whether you're exploring the newest emojis from the latest iOS update or comparing how platforms differ, this page makes it easy to dive into Apple's creative emoji style. + Apple's emojis are known for their vibrant, playful designs and the + frequent updates that come with each iOS release. Instead of relying on + standard Unicode glyphs, Apple creates custom artwork for every emoji, + which makes their style instantly recognizable to iPhone, iPad, and Mac + users. Discover Apple's full emoji collection here, complete with + previews and instant copy options for every category. Whether you're + exploring the newest emojis from the latest iOS update or comparing how + platforms differ, this page makes it easy to dive into Apple's creative + emoji style.

-
- {paginatedCategories.map((category) => ( -
-
-
- {category +
+ { + paginatedCategories.map((category) => ( +
+ +

+ {( + categoryCounts[category] || emojisByCategory[category].length + ).toLocaleString()}{' '} + emojis available +

+ +
+ {emojisByCategory[category].slice(0, 5).map((emoji: any) => { + const emojiName = emoji.title || emoji.slug; + const truncatedName = + emojiName.length > 27 + ? emojiName.substring(0, 27) + '...' + : emojiName; + return ( + + {emoji.code || emoji.emoji} {truncatedName} + + ); + })} + {(categoryCounts[category] || emojisByCategory[category].length) > + 5 && ( +

+ + + {( + (categoryCounts[category] || + emojisByCategory[category].length) - 5 + ).toLocaleString()}{' '} + more items +

+ )} +
+ + - - {category} -
-

- {emojisByCategory[category].length} emojis available -

-
- ))} + )) + }
- diff --git a/frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts b/frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts index 192c703b89..f632acdf38 100644 --- a/frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts +++ b/frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts @@ -1,9 +1,14 @@ -import { getAllAppleEmojis } from "@/lib/emojis"; import type { APIRoute } from "astro"; +import { getSitemapAppleEmojis } from "db/emojis/emojis-utils"; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); + // Use site from .env file (SITE variable) or astro.config.mjs + const envSite = process.env.SITE; + const siteStr = site?.toString() || ''; + const siteUrl = envSite || siteStr || 'http://localhost:4321/freedevtools'; + // Predefined allowed categories const allowedCategories = [ "Activities", @@ -24,20 +29,20 @@ export const GET: APIRoute = async ({ site }) => { ) ); - // Fetch Apple emojis - const emojis = getAllAppleEmojis(); + // Fetch Apple emojis (lightweight - only slug and category) + const emojis = await getSitemapAppleEmojis(); const urls: string[] = []; // Landing Page urls.push( - ` \n ${site}/emojis/apple-emojis/\n ${now}\n daily\n 0.9\n ` + ` \n ${siteUrl}/emojis/apple-emojis/\n ${now}\n daily\n 0.9\n ` ); // Category pages (only allowed categories) const categories = new Set(); for (const e of emojis) { - const cat = (e as any).category as string | undefined; + const cat = e.category; if (!cat) continue; const slug = cat.toLowerCase().replace(/[^a-z0-9]+/g, "-"); @@ -49,7 +54,7 @@ export const GET: APIRoute = async ({ site }) => { for (const cat of Array.from(categories)) { urls.push( - ` \n ${site}/emojis/apple-emojis/${cat}/\n ${now}\n daily\n 0.8\n ` + ` \n ${siteUrl}/emojis/apple-emojis/${cat}/\n ${now}\n daily\n 0.8\n ` ); } @@ -58,7 +63,7 @@ export const GET: APIRoute = async ({ site }) => { if (!e.slug) continue; urls.push( - ` \n ${site}/emojis/apple-emojis/${e.slug}/\n ${now}\n daily\n 0.8\n ` + ` \n ${siteUrl}/emojis/apple-emojis/${e.slug}/\n ${now}\n daily\n 0.8\n ` ); } diff --git a/frontend/src/pages/emojis/discord-emojis/[category].astro b/frontend/src/pages/emojis/discord-emojis/[category].astro index 82a4ca0b6b..4e3eb791c7 100644 --- a/frontend/src/pages/emojis/discord-emojis/[category].astro +++ b/frontend/src/pages/emojis/discord-emojis/[category].astro @@ -5,98 +5,212 @@ import Pagination from '../../../components/PaginationComponent.astro'; import CreditsButton from '../../../components/buttons/CreditsButton'; import ToolContainer from '../../../components/tool/ToolContainer'; import ToolHead from '../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestDiscordImage } from '../../../lib/emojis'; - -// Generate static paths -export async function getStaticPaths() { - const categories = getEmojiCategories(); - return categories - .filter((cat) => cat && cat.trim() !== '') - .map((cat) => ({ - params: { category: cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') }, - props: { category: cat }, - })); -} +import { getEmojiCategories, getEmojiBySlug, getDiscordEmojiBySlug, getEmojisByCategoryWithDiscordImagesPaginated } from 'db/emojis/emojis-utils'; +import VendorEmojiPage from '@/components/VendorEmojiPage'; +import { apple_vendor_excluded_emojis } from '@/lib/emojis-consts'; + +export const prerender = false; const { category: categorySlug } = Astro.params; -const categoryName = - (Astro.props?.category as string) || - categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); - -// Load Discord emojis -const emojis = (await getEmojisByCategory(categoryName, "discord")) - .map(e => ({ - ...e, - latestDiscordImage: fetchLatestDiscordImage(e.slug) - })) - // ❌ If image doesn't exist → remove the emoji entirely - .filter(e => e.latestDiscordImage); - - -if (emojis.length === 0) { - return Astro.redirect('/freedevtools/emojis/discord-emojis/'); +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); } -// Discord-specific category descriptions -export const discordCategoryDescriptions = { +// Get all categories to validate +const allCategories = await getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); + +// Check if category is numeric (pagination route) - 404 since there's no pagination on discord-emojis index +if (categorySlug && /^\d+$/.test(categorySlug)) { + return new Response(null, { status: 404 }); +} + +let emojiData: any = null; +let categoryData: any = null; + +// Check if this is actually an emoji slug (not a category) +// Since [category].astro matches first, we need to handle emoji slugs here +const emoji = await getEmojiBySlug(categorySlug!); +if (emoji && !categorySlugs.includes(categorySlug!)) { + // This is an emoji slug, not a category - handle it here + const discordEmoji = await getDiscordEmojiBySlug(categorySlug!); + + if (!discordEmoji) { + return new Response(null, { status: 404 }); + } + + const cleanDescription = (text?: string) => { + if (!text) return ''; + return text + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/[?]{2,}/g, '') + .trim(); + }; + + const cleanedDescription = cleanDescription(discordEmoji.discord_vendor_description || discordEmoji.description); + + emojiData = { + emoji: discordEmoji, + cleanedDescription, + }; +} else { + // Validate category exists + if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); + } + + // Find the actual category name from the slug + const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug + ) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); + + // Discord-specific category descriptions + const discordCategoryDescriptions: Record = { "Smileys & Emotion": "Discord's expressive faces use clean shapes and bold outlines, tailored for clarity in chats and dark mode environments.", "People & Body": "Discord-style characters emphasize cartoon-like simplicity with sharp lines, making gestures and skin tone variations easy to identify.", "Animals & Nature": "Discord animals have a flat, modern aesthetic with vibrant colors that look crisp across devices.", "Food & Drink": "Food emojis on Discord use simplified shading and bold silhouettes for easy visibility in small message bubbles.", - "Travel & Places": "Discord’s travel icons are minimalist, relying on clear geometry and strong contrast, ensuring readability across themes.", + "Travel & Places": "Discord's travel icons are minimalist, relying on clear geometry and strong contrast, ensuring readability across themes.", "Activities": "Activity emojis feature flat color palettes and high contrast for clear visual communication during events or streams.", - "Objects": "Objects follow Discord’s modern-icon style — clean, bold, and optimized for chat environments.", + "Objects": "Objects follow Discord's modern-icon style — clean, bold, and optimized for chat environments.", "Symbols": "Discord symbols are designed with strong contrast and flat color styles that stand out in dark mode.", "Flags": "Flags on Discord are simplified yet recognizable, ensuring quick identification without excessive detail.", - "Other": "Miscellaneous emojis follow the same sharp, modern style that defines Discord’s visual identity." -}; - -// Pagination -const totalEmojis = emojis.length; -const itemsPerPage = 36; -const currentPage = 1; -const totalPages = Math.ceil(totalEmojis / itemsPerPage); -const paginatedEmojis = emojis.slice(0, itemsPerPage); - -// Breadcrumbs -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: 'Discord Emojis', href: '/freedevtools/emojis/discord-emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/discord-emojis/${categorySlug}/` } -]; + "Other": "Miscellaneous emojis follow the same sharp, modern style that defines Discord's visual identity." + }; + + // Pagination (optimized query) + const itemsPerPage = 36; + const currentPage = 1; + const { emojis: paginatedEmojis, total: totalEmojis } = await getEmojisByCategoryWithDiscordImagesPaginated(categoryName, currentPage, itemsPerPage); + const totalPages = Math.ceil(totalEmojis / itemsPerPage); + + if (paginatedEmojis.length === 0) { + return Astro.redirect('/freedevtools/emojis/discord-emojis/'); + } + + // Breadcrumbs + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + { label: 'Discord Emojis', href: '/freedevtools/emojis/discord-emojis/' }, + { label: categoryName, href: `/freedevtools/emojis/discord-emojis/${categorySlug}/` } + ]; + + categoryData = { + categoryName, + categorySlug, + totalEmojis, + itemsPerPage, + currentPage, + totalPages, + paginatedEmojis, + breadcrumbItems, + discordCategoryDescriptions, + }; +} --- - +{emojiData ? ( + +
+
+ +
+ +
+ +
+ + + + +
+
+ + ← Back to Discord Emojis + + + {emojiData.emoji.category ? ( + + View {emojiData.emoji.category.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())} Category + + ) : null} + + {emojiData.emoji.apple_vendor_description && + !apple_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( + + 🍎 View Apple Version + + ) : null} +
+
+
+
+) : categoryData ? ( +
-
{totalEmojis.toLocaleString()}
+
{categoryData.totalEmojis.toLocaleString()}
Emojis
-
{totalPages}
+
{categoryData.totalPages}
Pages
@@ -105,13 +219,13 @@ const breadcrumbItems = [
- Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page {currentPage} of {totalPages}) + Showing {categoryData.paginatedEmojis.length} of {categoryData.totalEmojis} emojis (Page {categoryData.currentPage} of {categoryData.totalPages})
- {paginatedEmojis.map((emoji) => { + {categoryData.paginatedEmojis.map((emoji) => { const emojiChar = emoji.code || ''; const graphemeCount = [...emojiChar].length; const zwjCount = (emojiChar.match(/\u200d/g) || []).length; @@ -153,9 +267,9 @@ const breadcrumbItems = [ @@ -172,6 +286,7 @@ const breadcrumbItems = [
+) : null} diff --git a/frontend/src/pages/emojis/discord-emojis/[slug].astro b/frontend/src/pages/emojis/discord-emojis/[slug].astro deleted file mode 100644 index a602c851d6..0000000000 --- a/frontend/src/pages/emojis/discord-emojis/[slug].astro +++ /dev/null @@ -1,102 +0,0 @@ ---- -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import AdBanner from '../../../components/banner/AdBanner'; -import { getAllDiscordEmojis, getDiscordEmojiBySlug } from '@/lib/emojis'; -import VendorEmojiPage from '@/components/VendorEmojiPage'; - -import { apple_vendor_excluded_emojis } from '@/lib/emojis-consts'; - -export async function getStaticPaths() { - const emojis = getAllDiscordEmojis(); - return emojis.map((emoji) => ({ - params: { slug: emoji.slug }, - props: { emoji } - })); -} - -const { slug } = Astro.params; -const emoji = getDiscordEmojiBySlug(slug!); - -if (!emoji) { - return Astro.redirect('/freedevtools/emojis/discord-emojis/'); -} - -const cleanDescription = (text?: string) => { - if (!text) return ''; - return text - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/[?]{2,}/g, '') - .trim(); -}; ---- - - -
-
- -
- -
- -
- - - - -
-
- - ← Back to Discord Emojis - - - { - emoji.category ? ( - - View {emoji.category.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())} Category - - ) : null - } - - { - emoji.apple_vendor_description && - !apple_vendor_excluded_emojis.includes(emoji.slug) ? ( - - 🍎 View Apple Version - - ) : null - } -
-
-
-
diff --git a/frontend/src/pages/emojis/discord-emojis/index.astro b/frontend/src/pages/emojis/discord-emojis/index.astro index 6f4ce86151..f0a614dcd9 100644 --- a/frontend/src/pages/emojis/discord-emojis/index.astro +++ b/frontend/src/pages/emojis/discord-emojis/index.astro @@ -1,34 +1,38 @@ --- -import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { + fetchImageFromDB, + getDiscordCategoriesWithPreviewEmojis, + getEmojiCategories, + type EmojiData, +} from 'db/emojis/emojis-utils'; import AdBanner from '../../../components/banner/AdBanner'; -import { getAllEmojis, getEmojiCategories, fetchImageFromDB } from '../../../lib/emojis'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; -// Load all emojis -const emojis = await getAllEmojis(); +// Get categories with preview emojis in a single optimized query +const categoriesWithPreview = await getDiscordCategoriesWithPreviewEmojis(5); +const categories = await getEmojiCategories(); -// Metadata-based categories -const categories = getEmojiCategories(); +// Transform to match expected format const emojisByCategory: Record = {}; - -// Group emojis by category -for (const cat of categories) { - emojisByCategory[cat] = emojis.filter((e) => (e.category || 'Other') === cat); +const categoryCounts: Record = {}; +for (const catWithPreview of categoriesWithPreview) { + emojisByCategory[catWithPreview.category] = catWithPreview.previewEmojis.map( + (e) => + ({ + code: e.code, + slug: e.slug, + title: e.title, + }) as EmojiData + ); + categoryCounts[catWithPreview.category] = catWithPreview.count; } -// Sort categories alphabetically and exclude "Other" -const sortedCategories = Object.keys(emojisByCategory) +// Sort categories alphabetically (already sorted from query, but ensure) +const sortedCategories = categoriesWithPreview + .map((c) => c.category) .filter((cat) => cat !== 'Other') .sort(); -// Sort emojis within each category by title (fallback to slug) -for (const category of sortedCategories) { - emojisByCategory[category].sort((a, b) => { - const titleA = a.title || a.slug || ''; - const titleB = b.title || b.slug || ''; - return titleA.localeCompare(titleB); - }); -} - // Pagination logic for page 1 const itemsPerPage = 30; const currentPage = 1; @@ -42,21 +46,24 @@ const totalCategories = sortedCategories.length; const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: 'Discord Emojis' } + { label: 'Discord Emojis' }, ]; // SEO data -const seoTitle = currentPage === 1 - ? "Discord Emojis Reference - Browse & Copy Discord Emojis | Online Free DevTools by Hexmos" - : `Discord Emojis Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; +const seoTitle = + currentPage === 1 + ? 'Discord Emojis Reference - Browse & Copy Discord Emojis | Online Free DevTools by Hexmos' + : `Discord Emojis Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = currentPage === 1 - ? "Browse Discord-styled emojis by category. Copy instantly, view Discord-style artwork, and compare them with other platform vendors." - : `Browse page ${currentPage} of the Discord emoji reference with instant copy and full category browsing.`; +const seoDescription = + currentPage === 1 + ? 'Browse Discord-styled emojis by category. Copy instantly, view Discord-style artwork, and compare them with other platform vendors.' + : `Browse page ${currentPage} of the Discord emoji reference with instant copy and full category browsing.`; -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/emojis/discord-emojis/" - : `https://hexmos.com/freedevtools/emojis/discord-emojis/${currentPage}/`; +const canonical = + currentPage === 1 + ? 'https://hexmos.com/freedevtools/emojis/discord-emojis/' + : `https://hexmos.com/freedevtools/emojis/discord-emojis/${currentPage}/`; const mainKeywords = [ 'discord emojis', @@ -67,27 +74,59 @@ const mainKeywords = [ 'emoji meanings', 'emoji shortcodes', 'emoji library', - 'emoji copy' + 'emoji copy', ]; // Category icons — using Discord vendor images -export const categoryIconMap: Record = { - "Smileys & Emotion": fetchImageFromDB("slightly-smiling-face", "slightly-smiling-face_1f642_twitter_15.0.3.png")!, - "People & Body": fetchImageFromDB("bust-in-silhouette", "bust-in-silhouette_1f464_twitter_15.0.3.png")!, - "Animals & Nature": fetchImageFromDB("dog-face", "dog-face_1f436_twitter_15.0.3.png")!, - "Food & Drink": fetchImageFromDB("red-apple", "red-apple_1f34e_twitter_15.0.3.png")!, - "Travel & Places": fetchImageFromDB("airplane", "airplane_2708-fe0f_twitter_15.0.3.png")!, - "Activities": fetchImageFromDB("soccer-ball", "soccer-ball_26bd_twitter_15.0.3.png")!, - "Objects": fetchImageFromDB("mobile-phone", "mobile-phone_1f4f1_twitter_15.0.3.png")!, - "Symbols": fetchImageFromDB("check-mark-button", "check-mark-button_2705_twitter_15.0.3.png")!, - "Flags": fetchImageFromDB("chequered-flag", "chequered-flag_1f3c1_twitter_15.0.3.png")!, - "Other": fetchImageFromDB("question-mark", "question-mark_2753_twitter_15.0.3.png")!, +const categoryIconMap: Record = { + 'Smileys & Emotion': (await fetchImageFromDB( + 'slightly-smiling-face', + 'slightly-smiling-face_1f642_twitter_15.0.3.png' + )) || '', + 'People & Body': (await fetchImageFromDB( + 'bust-in-silhouette', + 'bust-in-silhouette_1f464_twitter_15.0.3.png' + )) || '', + 'Animals & Nature': (await fetchImageFromDB( + 'dog-face', + 'dog-face_1f436_twitter_15.0.3.png' + )) || '', + 'Food & Drink': (await fetchImageFromDB( + 'red-apple', + 'red-apple_1f34e_twitter_15.0.3.png' + )) || '', + 'Travel & Places': (await fetchImageFromDB( + 'airplane', + 'airplane_2708-fe0f_twitter_15.0.3.png' + )) || '', + Activities: (await fetchImageFromDB( + 'soccer-ball', + 'soccer-ball_26bd_twitter_15.0.3.png' + )) || '', + Objects: (await fetchImageFromDB( + 'mobile-phone', + 'mobile-phone_1f4f1_twitter_15.0.3.png' + )) || '', + Symbols: (await fetchImageFromDB( + 'check-mark-button', + 'check-mark-button_2705_twitter_15.0.3.png' + )) || '', + Flags: (await fetchImageFromDB( + 'chequered-flag', + 'chequered-flag_1f3c1_twitter_15.0.3.png' + )) || '', + Other: (await fetchImageFromDB( + 'question-mark', + 'question-mark_2753_twitter_15.0.3.png' + )) || '', }; --- - = { partOf="Free DevTools" partOfUrl="https://hexmos.com/freedevtools/" keywords={mainKeywords} - features={["Discord-style designs", "Browse by category", "Copy instantly", "Compare with Unicode", "Free access"]} + features={[ + 'Discord-style designs', + 'Browse by category', + 'Copy instantly', + 'Compare with Unicode', + 'Free access', + ]} emojiCategory="Discord Emojis" >
@@ -111,16 +156,23 @@ export const categoryIconMap: Record = {

@@ -128,51 +180,103 @@ export const categoryIconMap: Record = {

- Discord's emoji style is clean, bold, and designed for chat. - These emojis look sharp and expressive, which makes them perfect for fast-paced conversations. - Explore the full Discord emoji collection here, sorted by category with quick copy options. + Discord's emoji style is clean, bold, and designed for chat. These + emojis look sharp and expressive, which makes them perfect for + fast-paced conversations. Explore the full Discord emoji collection + here, sorted by category with quick copy options.

-
- {paginatedCategories.map((category) => ( -
-
-
- {category +
+ { + paginatedCategories.map((category) => ( +
+ +

+ {( + categoryCounts[category] || emojisByCategory[category].length + ).toLocaleString()}{' '} + emojis available +

+ +
+ {emojisByCategory[category].slice(0, 5).map((emoji: any) => { + const emojiName = emoji.title || emoji.slug; + const truncatedName = + emojiName.length > 27 + ? emojiName.substring(0, 27) + '...' + : emojiName; + return ( + + {emoji.code || emoji.emoji} {truncatedName} + + ); + })} + {(categoryCounts[category] || emojisByCategory[category].length) > + 5 && ( +

+ + + {( + (categoryCounts[category] || + emojisByCategory[category].length) - 5 + ).toLocaleString()}{' '} + more items +

+ )} +
+ + - - {category} -
-

- {emojisByCategory[category].length} emojis available -

-
- ))} + )) + }
-
+
diff --git a/frontend/src/pages/emojis/discord-emojis/sitemap.xml.ts b/frontend/src/pages/emojis/discord-emojis/sitemap.xml.ts index 189077faa5..a48f36da22 100644 --- a/frontend/src/pages/emojis/discord-emojis/sitemap.xml.ts +++ b/frontend/src/pages/emojis/discord-emojis/sitemap.xml.ts @@ -1,9 +1,14 @@ -import { getAllDiscordEmojis } from "@/lib/emojis"; import type { APIRoute } from "astro"; +import { getSitemapDiscordEmojis } from "db/emojis/emojis-utils"; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); + // Use site from .env file (SITE variable) or astro.config.mjs + const envSite = process.env.SITE; + const siteStr = site?.toString() || ''; + const siteUrl = envSite || siteStr || 'http://localhost:4321/freedevtools'; + // Predefined allowed categories (as given) const allowedCategories = [ "Activities", @@ -24,20 +29,20 @@ export const GET: APIRoute = async ({ site }) => { ) ); - // Fetch Discord emojis - const emojis = getAllDiscordEmojis(); + // Fetch Discord emojis (lightweight - only slug and category) + const emojis = await getSitemapDiscordEmojis(); const urls: string[] = []; // Landing Page urls.push( - ` \n ${site}/emojis/discord-emojis/\n ${now}\n daily\n 0.9\n ` + ` \n ${siteUrl}/emojis/discord-emojis/\n ${now}\n daily\n 0.9\n ` ); // Collect emoji categories only if they belong to allowed ones const categories = new Set(); for (const e of emojis) { - const cat = (e as any).category as string | undefined; + const cat = e.category; if (!cat) continue; const slug = cat.toLowerCase().replace(/[^a-z0-9]+/g, "-"); @@ -50,7 +55,7 @@ export const GET: APIRoute = async ({ site }) => { // Per-category URLs (only allowed categories) for (const cat of Array.from(categories)) { urls.push( - ` \n ${site}/emojis/discord-emojis/${cat}/\n ${now}\n daily\n 0.8\n ` + ` \n ${siteUrl}/emojis/discord-emojis/${cat}/\n ${now}\n daily\n 0.8\n ` ); } @@ -58,7 +63,7 @@ export const GET: APIRoute = async ({ site }) => { for (const e of emojis) { if (!e.slug) continue; urls.push( - ` \n ${site}/emojis/discord-emojis/${e.slug}/\n ${now}\n daily\n 0.8\n ` + ` \n ${siteUrl}/emojis/discord-emojis/${e.slug}/\n ${now}\n daily\n 0.8\n ` ); } diff --git a/frontend/src/pages/emojis/index.astro b/frontend/src/pages/emojis/index.astro index 3727383cc5..d0bca9b279 100644 --- a/frontend/src/pages/emojis/index.astro +++ b/frontend/src/pages/emojis/index.astro @@ -1,33 +1,41 @@ --- +import { + getCategoriesWithPreviewEmojis, + getEmojiCategories, + getTotalEmojis, + type EmojiData, +} from 'db/emojis/emojis-utils'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getAllEmojis, getEmojiCategories } from '../../lib/emojis'; import EmojiPage from './_EmojiPage.astro'; -const emojis = await getAllEmojis(); +// Get all data BEFORE rendering starts to prevent stream closure errors +const [categoriesWithPreview, categories, totalEmojis] = await Promise.all([ + getCategoriesWithPreviewEmojis(5), + getEmojiCategories(), + getTotalEmojis(), +]); -// Metadata-based categories -const categories = getEmojiCategories(); +// Transform to match expected format const emojisByCategory: Record = {}; - -// Group emojis by category -for (const cat of categories) { - emojisByCategory[cat] = emojis.filter((e) => (e.category || 'Other') === cat); +const categoryCounts: Record = {}; +for (const catWithPreview of categoriesWithPreview) { + emojisByCategory[catWithPreview.category] = catWithPreview.previewEmojis.map( + (e) => + ({ + code: e.code, + slug: e.slug, + title: e.title, + }) as EmojiData + ); + categoryCounts[catWithPreview.category] = catWithPreview.count; } -// Sort categories alphabetically and exclude "Other" -const sortedCategories = Object.keys(emojisByCategory) +// Sort categories alphabetically (already sorted from query, but ensure) +const sortedCategories = categoriesWithPreview + .map((c) => c.category) .filter((cat) => cat !== 'Other') .sort(); -// Sort emojis within each category by title (fallback to slug) -for (const category of sortedCategories) { - emojisByCategory[category].sort((a, b) => { - const titleA = a.title || a.slug || ''; - const titleB = b.title || b.slug || ''; - return titleA.localeCompare(titleB); - }); -} - // Pagination logic for page 1 const itemsPerPage = 30; const currentPage = 1; @@ -35,27 +43,29 @@ const totalPages = Math.ceil(sortedCategories.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const paginatedCategories = sortedCategories.slice(startIndex, endIndex); -const totalCategories = sortedCategories.length - +const totalCategories = sortedCategories.length; // Breadcrumb items const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis' } + { label: 'Emojis' }, ]; // SEO data -const seoTitle = currentPage === 1 - ? "Emoji Reference - Browse & Copy Emojis | Online Free DevTools by Hexmos" - : `Emoji Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; +const seoTitle = + currentPage === 1 + ? 'Emoji Reference - Browse & Copy Emojis | Online Free DevTools by Hexmos' + : `Emoji Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = currentPage === 1 - ? "Explore the emoji reference by category. Find meanings, names, and shortcodes. Browse thousands of emojis and copy instantly. Free, fast, no signup." - : `Browse page ${currentPage} of our emoji reference. Find meanings, names, and shortcodes. Copy emojis instantly.`; +const seoDescription = + currentPage === 1 + ? 'Explore the emoji reference by category. Find meanings, names, and shortcodes. Browse thousands of emojis and copy instantly. Free, fast, no signup.' + : `Browse page ${currentPage} of our emoji reference. Find meanings, names, and shortcodes. Copy emojis instantly.`; -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/emojis/" - : `https://hexmos.com/freedevtools/emojis/${currentPage}/`; +const canonical = + currentPage === 1 + ? 'https://hexmos.com/freedevtools/emojis/' + : `https://hexmos.com/freedevtools/emojis/${currentPage}/`; // Enhanced keywords for main page const mainKeywords = [ @@ -68,18 +78,18 @@ const mainKeywords = [ 'free emojis', 'emoji search', 'emoji copy', - 'unicode emojis' + 'unicode emojis', ]; --- - - diff --git a/frontend/src/pages/emojis/sitemap.xml.ts b/frontend/src/pages/emojis/sitemap.xml.ts index 609c44284b..3cf7c1f59c 100644 --- a/frontend/src/pages/emojis/sitemap.xml.ts +++ b/frontend/src/pages/emojis/sitemap.xml.ts @@ -2,8 +2,14 @@ import type { APIRoute } from "astro"; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); - const { getAllEmojis } = await import("@/lib/emojis"); - const emojis = getAllEmojis(); + + // Use site from .env file (SITE variable) or astro.config.mjs + const envSite = process.env.SITE; + const siteStr = site?.toString() || ''; + const siteUrl = envSite || siteStr || 'http://localhost:4321/freedevtools'; + + const { getSitemapEmojis } = await import("db/emojis/emojis-utils"); + const emojis = await getSitemapEmojis(); // Predefined allowed categories const allowedCategories = [ @@ -29,19 +35,14 @@ export const GET: APIRoute = async ({ site }) => { // Category landing urls.push( - ` \n ${site}/emojis/\n ${now}\n daily\n 0.9\n ` + ` \n ${siteUrl}/emojis/\n ${now}\n daily\n 0.9\n ` ); // Per-category pages (only allowed ones) const categories = new Set(); for (const e of emojis) { - const cat = - e.fluentui_metadata?.group || - e.emoji_net_data?.category || - (e as any).given_category || - "other"; - + const cat = e.category || "other"; const slug = cat.toLowerCase().replace(/[^a-z0-9]+/g, "-"); if (allowedSlugs.has(slug)) { @@ -51,7 +52,7 @@ export const GET: APIRoute = async ({ site }) => { for (const cat of Array.from(categories)) { urls.push( - ` \n ${site}/emojis/${cat}/\n ${now}\n daily\n 0.8\n ` + ` \n ${siteUrl}/emojis/${cat}/\n ${now}\n daily\n 0.8\n ` ); } @@ -60,7 +61,7 @@ export const GET: APIRoute = async ({ site }) => { if (!e.slug) continue; urls.push( - ` \n ${site}/emojis/${e.slug}/\n ${now}\n daily\n 0.8\n ` + ` \n ${siteUrl}/emojis/${e.slug}/\n ${now}\n daily\n 0.8\n ` ); } diff --git a/frontend/src/pages/man-pages/[category]/[page].astro b/frontend/src/pages/man-pages/[category]/[page].astro deleted file mode 100644 index 227a94ba08..0000000000 --- a/frontend/src/pages/man-pages/[category]/[page].astro +++ /dev/null @@ -1,155 +0,0 @@ ---- -import AdBanner from '@/components/banner/AdBanner.astro'; -import CreditsButton from '@/components/buttons/CreditsButton'; -import Pagination from '@/components/PaginationComponent.astro'; -import ToolContainer from '@/components/tool/ToolContainer'; -import ToolHead from '@/components/tool/ToolHead'; -import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getSubCategoriesByMainCategory, getSubCategoriesCountByMainCategory, getSubCategoriesByMainCategoryPaginated, getTotalManPagesCountByMainCategory, generateCategoryStaticPaths } from '@/lib/man-pages-utils'; - -// Generate static paths for paginated category routes -export async function getStaticPaths() { - const categoryPaths = generateCategoryStaticPaths(); - const paths: any[] = []; - - for (const categoryPath of categoryPaths) { - const { category } = categoryPath.params; - - // Get subcategories count for this category to determine pagination - const totalSubcategoriesCount = getSubCategoriesCountByMainCategory(category); - const itemsPerPage = 12; - const totalPages = Math.ceil(totalSubcategoriesCount / itemsPerPage); - - // Generate pagination pages (page 2, 3, 4, etc. - page 1 is handled by index.astro) - for (let page = 2; page <= totalPages; page++) { - paths.push({ - params: { - category: category, - page: page.toString() - }, - props: { - currentPage: page, - } - }); - } - } - - return paths; -} - -const { currentPage } = Astro.props; -const { category } = Astro.params; - -// Calculate pagination -const itemsPerPage = 12; -const offset = (currentPage - 1) * itemsPerPage; - -// Get ONLY the subcategories for current page from database (efficient!) -const currentPageSubcategories = getSubCategoriesByMainCategoryPaginated(category, itemsPerPage, offset); -const totalSubcategoriesCount = getSubCategoriesCountByMainCategory(category); -const totalManPagesInCategory = getTotalManPagesCountByMainCategory(category); -const totalPages = Math.ceil(totalSubcategoriesCount / itemsPerPage); - -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Man Pages', href: '/freedevtools/man-pages/' }, - { label: category.replace('-', ' ').charAt(0).toUpperCase() + category.replace('-', ' ').slice(1) }, - { label: `Page ${currentPage}` }, -]; - -const categoryTitle = category.replace('-', ' ').charAt(0).toUpperCase() + category.replace('-', ' ').slice(1); ---- - - - -
- -
- - - -
-
-
-
{totalSubcategoriesCount >= 1000 ? Math.round(totalSubcategoriesCount / 1000) + 'k' : totalSubcategoriesCount}
-
Subcategories
-
-
-
{totalManPagesInCategory >= 1000 ? Math.round(totalManPagesInCategory / 1000) + 'k' : totalManPagesInCategory}
-
Man Pages
-
-
-
- - -
-
- Showing {currentPageSubcategories.length} of {totalSubcategoriesCount} subcategories (Page {currentPage} of {totalPages}) -
-
- - - - - - - - - -
-
\ No newline at end of file diff --git a/frontend/src/pages/man-pages/[category]/[subcategory]/[slug].astro b/frontend/src/pages/man-pages/[category]/[subcategory]/[slug].astro index 74be6e4f16..2b09e89f65 100644 --- a/frontend/src/pages/man-pages/[category]/[subcategory]/[slug].astro +++ b/frontend/src/pages/man-pages/[category]/[subcategory]/[slug].astro @@ -1,21 +1,8 @@ --- -import AdBanner from '@/components/banner/AdBanner.astro'; -import CreditsButton from '@/components/buttons/CreditsButton'; -import ToolContainer from '@/components/tool/ToolContainer'; -import ToolHead from '@/components/tool/ToolHead'; -import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getManPageBySlug, generateCommandStaticPaths } from '@/lib/man-pages-utils'; -import SeeAlsoIndex from '@/components/seealso/SeeAlsoIndex.astro'; +import PaginationView from './_SubCategoryPagination.astro'; +import PageView from './_Page.astro'; -export async function getStaticPaths() { - try { - const paths = generateCommandStaticPaths(); - return paths; - } catch (error) { - console.error('Error generating static paths:', error); - throw error; - } -} +export const prerender = false; const { category, subcategory, slug } = Astro.params; @@ -24,117 +11,14 @@ if (!category || !subcategory || !slug) { return Astro.redirect('/freedevtools/man-pages/'); } -// Get man page from database by slug -const manPage = getManPageBySlug(category, subcategory, slug); - -if (!manPage) { - // Redirect to man pages index if not found - return Astro.redirect('/freedevtools/man-pages/'); -} - -// Generate table of contents from sections -const tocSections = Object.keys(manPage.content).map(section => ({ - id: section.toLowerCase(), - label: section, -})); - -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Man Pages', href: '/freedevtools/man-pages/' }, - { label: manPage.main_category.replace('-', ' ').charAt(0).toUpperCase() + manPage.main_category.replace('-', ' ').slice(1), href: `/freedevtools/man-pages/${manPage.main_category}/` }, - { label: manPage.sub_category.replace('-', ' ').charAt(0).toUpperCase() + manPage.sub_category.replace('-', ' ').slice(1), href: `/freedevtools/man-pages/${manPage.main_category}/${manPage.sub_category}/` }, - { label: manPage.slug, href: `/freedevtools/man-pages/${manPage.main_category}/${manPage.sub_category}/${manPage.slug}/` }, -]; +// Check if slug is numeric (pagination) +const isNumericPage = /^\d+$/.test(slug); --- - - -
- -
- - - -
-

Contents

- -
- - -
- -
- {Object.entries(manPage.content).map(([section, content]) => ( -
-

- {section} -

-
-
- ))} -
-
- - - - - -
-
- - \ No newline at end of file +{isNumericPage ? ( + // If numeric, it's a pagination page eg: /man-pages/linux/commands/2/ + +) : ( + // If not numeric, it's a man page eg: /man-pages/linux/commands/ddttrf/ + +)} diff --git a/frontend/src/pages/man-pages/[category]/[subcategory]/_CategoryPagination.astro b/frontend/src/pages/man-pages/[category]/[subcategory]/_CategoryPagination.astro new file mode 100644 index 0000000000..b4f4f1bc99 --- /dev/null +++ b/frontend/src/pages/man-pages/[category]/[subcategory]/_CategoryPagination.astro @@ -0,0 +1,176 @@ +--- +import AdBanner from '@/components/banner/AdBanner.astro'; +import CreditsButton from '@/components/buttons/CreditsButton'; +import Pagination from '@/components/PaginationComponent.astro'; +import ToolContainer from '@/components/tool/ToolContainer'; +import ToolHead from '@/components/tool/ToolHead'; +import BaseLayout from '@/layouts/BaseLayout.astro'; +import { + getTotalSubCategoriesManPagesCount, + getSubCategoriesByMainCategoryPaginated, +} from 'db/man_pages/man-pages-utils'; +import { formatName } from '@/lib/utils'; + +const { category, page } = Astro.props; + + +// This handles /man-pages/user-commands/2/ (Page 2 of subcategories list) + +const currentPage = parseInt(page, 10); + +// Get subcategories count for this category to determine pagination +const {man_pages_count, sub_category_count} = await getTotalSubCategoriesManPagesCount(category); + + +const itemsPerPage = 12; +const totalPages = Math.ceil(sub_category_count / itemsPerPage); +// Redirect if page is out of range or invalid +if (currentPage < 1 || (totalPages > 0 && currentPage > totalPages)) { + return Astro.redirect(`/freedevtools/man-pages/${category}/`); +} + +// Calculate pagination +const offset = (currentPage - 1) * itemsPerPage; + +// Get ONLY the subcategories for current page from database (efficient!) +const subcategories = await getSubCategoriesByMainCategoryPaginated( + category, + itemsPerPage, + offset +); + +const categoryTitle = formatName(category); + +const pageTitle = `${categoryTitle} Man Pages - Page ${currentPage}`; +const pageDescription = `Browse ${categoryTitle} manual page subcategories - Page ${currentPage} of ${totalPages}. Find detailed documentation and system references.`; + +const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Man Pages', href: '/freedevtools/man-pages/' }, + { label: categoryTitle, href: `/freedevtools/man-pages/${category}/` }, + { label: `Page ${currentPage}` }, +]; +--- + + + +
+ +
+ + + +
+
+
+
+ { + sub_category_count >= 1000 + ? Math.round(sub_category_count / 1000) + 'k' + : sub_category_count + } +
+
+ Subcategories +
+
+
+
+ {man_pages_count >= 1000 + ? Math.round(man_pages_count / 1000) + 'k' + : man_pages_count} +
+
+ Man Pages +
+
+
+
+ + +
+
+ Showing {subcategories.length} of {sub_category_count} subcategories (Page {currentPage} of {totalPages}) +
+
+ + + + + + { + sub_category_count === 0 && ( +
+

+ No subcategories found. +

+
+ ) + } + + + + + + +
+
diff --git a/frontend/src/pages/man-pages/[category]/[subcategory]/_Page.astro b/frontend/src/pages/man-pages/[category]/[subcategory]/_Page.astro new file mode 100644 index 0000000000..a9c5eb87b3 --- /dev/null +++ b/frontend/src/pages/man-pages/[category]/[subcategory]/_Page.astro @@ -0,0 +1,146 @@ +--- +import AdBanner from '@/components/banner/AdBanner.astro'; +import CreditsButton from '@/components/buttons/CreditsButton'; +import SeeAlsoIndex from '@/components/seealso/SeeAlsoIndex.astro'; +import ToolContainer from '@/components/tool/ToolContainer'; +import ToolHead from '@/components/tool/ToolHead'; +import BaseLayout from '@/layouts/BaseLayout.astro'; +import { getManPageBySlug } from 'db/man_pages/man-pages-utils'; +import { formatName } from '@/lib/utils'; + + + +const { category, subcategory, slug } = Astro.props; + +const manPage = await getManPageBySlug(category, subcategory, slug); + +if (!manPage) { + // Redirect to man pages index if not found + return Astro.redirect('/freedevtools/man-pages/'); +} + +// Table of contents sections +const tocSections = Object.keys(manPage.content).map((section) => ({ + id: section.toLowerCase(), + label: section, +})); + +const pageDescription = `${manPage.title} manual page. Learn about ${manPage.title.split(' — ')[1]}`; + +const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Man Pages', href: '/freedevtools/man-pages/' }, + { + label: formatName(category), + href: `/freedevtools/man-pages/${category}/`, + }, + { + label: formatName(subcategory), + href: `/freedevtools/man-pages/${category}/${subcategory}/`, + }, + { + label: manPage.slug + }, +]; + +--- + + + +
+ +
+ + + +
+

Contents

+ +
+ + +
+ +
+ {Object.entries(manPage.content).map(([section, content]) => ( +
+

+ {section} +

+
+
+ ))} +
+
+ + + + + +
+
+ + diff --git a/frontend/src/pages/man-pages/[category]/[subcategory]/_SubCategory.astro b/frontend/src/pages/man-pages/[category]/[subcategory]/_SubCategory.astro new file mode 100644 index 0000000000..3207d4d0f6 --- /dev/null +++ b/frontend/src/pages/man-pages/[category]/[subcategory]/_SubCategory.astro @@ -0,0 +1,156 @@ +--- +import AdBanner from '@/components/banner/AdBanner.astro'; +import CreditsButton from '@/components/buttons/CreditsButton'; +import Pagination from '@/components/PaginationComponent.astro'; +import ToolContainer from '@/components/tool/ToolContainer'; +import ToolHead from '@/components/tool/ToolHead'; +import BaseLayout from '@/layouts/BaseLayout.astro'; +import { + getManPagesList, + getManPagesCountInSubCategory, +} from 'db/man_pages/man-pages-utils'; +import { formatName } from '@/lib/utils'; + +const { category, subcategory } = Astro.props; + +// This handles /man-pages/linux/commands/ (Page 1 of man pages list) +const itemsPerPage = 20; +const currentPage = 1; +const offset = (currentPage - 1) * itemsPerPage; + + +const items = await getManPagesList( + category, + subcategory, + itemsPerPage, + offset +); +const totalManPagesCount = await getManPagesCountInSubCategory( + category, + subcategory +); +const totalCount = totalManPagesCount; +const totalPages = Math.ceil(totalManPagesCount / itemsPerPage); + +const categoryTitle = formatName(category); +const subcategoryTitle = formatName(subcategory); + +const pageTitle = `${subcategoryTitle} Man Pages`; +const pageDescription = `Browse ${subcategoryTitle} manual pages in ${categoryTitle}. Find detailed documentation and system references.`; + +const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Man Pages', href: '/freedevtools/man-pages/' }, + { label: categoryTitle, href: `/freedevtools/man-pages/${category}/` }, + { label: subcategoryTitle }, +]; +--- + + + +
+ +
+ + + +
+
+
+
+ { + totalCount >= 1000 + ? Math.round(totalCount / 1000) + 'k' + : totalCount + } +
+
+ Man Pages in {subcategoryTitle} +
+
+
+
+ + +
+
+ Showing {items.length} of {totalCount} man pages (Page {currentPage} of {totalPages}) +
+
+ + + + + + { + totalCount === 0 && ( +
+

+ No man pages found. +

+
+ ) + } + + + + + + +
+
diff --git a/frontend/src/pages/man-pages/[category]/[subcategory]/[page].astro b/frontend/src/pages/man-pages/[category]/[subcategory]/_SubCategoryPagination.astro similarity index 57% rename from frontend/src/pages/man-pages/[category]/[subcategory]/[page].astro rename to frontend/src/pages/man-pages/[category]/[subcategory]/_SubCategoryPagination.astro index 84d6d83bad..f71fcf2927 100644 --- a/frontend/src/pages/man-pages/[category]/[subcategory]/[page].astro +++ b/frontend/src/pages/man-pages/[category]/[subcategory]/_SubCategoryPagination.astro @@ -5,69 +5,63 @@ import Pagination from '@/components/PaginationComponent.astro'; import ToolContainer from '@/components/tool/ToolContainer'; import ToolHead from '@/components/tool/ToolHead'; import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getManPagesCountBySubcategory, getManPagesBySubcategoryPaginated, generateSubcategoryStaticPaths } from '@/lib/man-pages-utils'; +import { + getManPagesList, + getManPagesCountInSubCategory, +} from 'db/man_pages/man-pages-utils'; +import { formatName } from '@/lib/utils'; -// Generate static paths for paginated subcategory routes -export async function getStaticPaths() { - const subcategoryPaths = generateSubcategoryStaticPaths(); - const paths: any[] = []; +const { category, subcategory, page } = Astro.props; - for (const subcategoryPath of subcategoryPaths) { - const { category, subcategory } = subcategoryPath.params; - - // Get total count of man pages for this subcategory to determine pagination - const totalManPagesCount = getManPagesCountBySubcategory(category, subcategory); - const itemsPerPage = 20; - const totalPages = Math.ceil(totalManPagesCount / itemsPerPage); - - // Generate pagination pages (page 2, 3, 4, etc. - page 1 is handled by index.astro) - for (let page = 2; page <= totalPages; page++) { - paths.push({ - params: { - category: category, - subcategory: subcategory, - page: page.toString() - }, - props: { - currentPage: page, - } - }); - } - } +const currentPage = parseInt(page, 10); - return paths; -} +// Get total count of man pages for this subcategory to determine pagination +const totalManPagesCount = await getManPagesCountInSubCategory( + category, + subcategory +); +const itemsPerPage = 20; +const totalPages = Math.ceil(totalManPagesCount / itemsPerPage); -const { currentPage } = Astro.props; -const { category, subcategory } = Astro.params; +// Redirect if page is out of range or invalid (optional but good practice) +if (currentPage < 1 || (totalPages > 0 && currentPage > totalPages)) { + return Astro.redirect( + `/freedevtools/man-pages/${category}/${subcategory}/` + ); +} // Calculate pagination -const itemsPerPage = 20; const offset = (currentPage - 1) * itemsPerPage; // Get ONLY the man pages for current page from database (efficient!) -const currentPageManPages = getManPagesBySubcategoryPaginated(category, subcategory, itemsPerPage, offset); -const totalManPagesCount = getManPagesCountBySubcategory(category, subcategory); -const totalPages = Math.ceil(totalManPagesCount / itemsPerPage); +const currentPageManPages = await getManPagesList( + category, + subcategory, + itemsPerPage, + offset +); + +const categoryTitle = formatName(category); +const subcategoryTitle = formatName(subcategory); + +const pageTitle = `${subcategoryTitle} Man Pages - Page ${currentPage}`; +const pageDescription = `Browse ${subcategoryTitle} manual pages - Page ${currentPage} of ${totalPages}. Find detailed documentation and system references.`; const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Man Pages', href: '/freedevtools/man-pages/' }, - { label: category.replace('-', ' ').charAt(0).toUpperCase() + category.replace('-', ' ').slice(1), href: `/freedevtools/man-pages/${category}/` }, - { label: subcategory.replace('-', ' ').charAt(0).toUpperCase() + subcategory.replace('-', ' ').slice(1), href: `/freedevtools/man-pages/${category}/${subcategory}/` }, + { label: categoryTitle, href: `/freedevtools/man-pages/${category}/` }, + { label: subcategoryTitle, href: `/freedevtools/man-pages/${category}/${subcategory}/` }, { label: `Page ${currentPage}` }, ]; - -const categoryTitle = category.replace('-', ' ').charAt(0).toUpperCase() + category.replace('-', ' ').slice(1); -const subcategoryTitle = subcategory.replace('-', ' ').charAt(0).toUpperCase() + subcategory.replace('-', ' ').slice(1); ---
- {currentPageManPages.map((manPage) => ( + {currentPageManPages.map((page) => (

- {manPage.title.length > 60 - ? manPage.title.slice(0, 57) + '...' - : manPage.title} + {page.title.length > 60 + ? page.title.slice(0, 57) + '...' + : page.title}

))} @@ -132,14 +126,14 @@ const subcategoryTitle = subcategory.replace('-', ' ').charAt(0).toUpperCase() + > View {categoryTitle} Category - +
-
\ No newline at end of file + diff --git a/frontend/src/pages/man-pages/[category]/[subcategory]/index.astro b/frontend/src/pages/man-pages/[category]/[subcategory]/index.astro index 948baf10f1..ae51234639 100644 --- a/frontend/src/pages/man-pages/[category]/[subcategory]/index.astro +++ b/frontend/src/pages/man-pages/[category]/[subcategory]/index.astro @@ -1,140 +1,22 @@ --- -import AdBanner from '@/components/banner/AdBanner.astro'; -import CreditsButton from '@/components/buttons/CreditsButton'; -import Pagination from '@/components/PaginationComponent.astro'; -import ToolContainer from '@/components/tool/ToolContainer'; -import ToolHead from '@/components/tool/ToolHead'; -import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getManPagesBySubcategory, getManPagesCountBySubcategory, getManPagesBySubcategoryPaginated, generateSubcategoryStaticPaths } from '@/lib/man-pages-utils'; +import CategoryPagination from './_CategoryPagination.astro'; +import SubCategory from './_SubCategory.astro'; -export async function getStaticPaths() { - return generateSubcategoryStaticPaths(); -} +export const prerender = false; const { category, subcategory } = Astro.params; if (!category || !subcategory) { - throw new Error('Category and subcategory parameters are required'); + return Astro.redirect('/freedevtools/man-pages/'); } -// Efficient pagination for page 1 -const itemsPerPage = 20; -const currentPage = 1; -const offset = (currentPage - 1) * itemsPerPage; // 0 for page 1 - -// Get ONLY the man pages for page 1 from database (efficient!) -const currentPageManPages = getManPagesBySubcategoryPaginated(category, subcategory, itemsPerPage, offset); -const totalManPagesCount = getManPagesCountBySubcategory(category, subcategory); -const totalPages = Math.ceil(totalManPagesCount / itemsPerPage); - -console.log("totalManPagesCount:", totalManPagesCount); -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Man Pages', href: '/freedevtools/man-pages/' }, - { label: category.replace('-', ' ').charAt(0).toUpperCase() + category.replace('-', ' ').slice(1), href: `/freedevtools/man-pages/${category}/` }, - { label: subcategory.replace('-', ' ').charAt(0).toUpperCase() + subcategory.replace('-', ' ').slice(1) }, -]; - -const categoryTitle = category.replace('-', ' ').charAt(0).toUpperCase() + category.replace('-', ' ').slice(1); -const subcategoryTitle = subcategory.replace('-', ' ').charAt(0).toUpperCase() + subcategory.replace('-', ' ').slice(1); +const isNumericPage = /^\d+$/.test(subcategory); --- - - - -
- -
- - - -
-
-
-
{totalManPagesCount >= 1000 ? Math.round(totalManPagesCount / 1000) + 'k' : totalManPagesCount}
-
Man Pages in {subcategoryTitle}
-
-
-
- - -
-
- Showing {currentPageManPages.length} of {totalManPagesCount} man pages (Page {currentPage} of {totalPages}) -
-
- - - - - - {totalManPagesCount === 0 && ( -
-

No man pages found in this subcategory.

-
- )} - - - - - - -
-
\ No newline at end of file + +{isNumericPage ? ( + +) : ( + // If subcategory is not numeric, it's a subcategory eg: /man-pages/user-commands/ + +)} diff --git a/frontend/src/pages/man-pages/[category]/index.astro b/frontend/src/pages/man-pages/[category]/index.astro index 1585d3e349..0d0e9d3327 100644 --- a/frontend/src/pages/man-pages/[category]/index.astro +++ b/frontend/src/pages/man-pages/[category]/index.astro @@ -5,16 +5,18 @@ import Pagination from '@/components/PaginationComponent.astro'; import ToolContainer from '@/components/tool/ToolContainer'; import ToolHead from '@/components/tool/ToolHead'; import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getSubCategoriesByMainCategory, getSubCategoriesCountByMainCategory, getSubCategoriesByMainCategoryPaginated, getTotalManPagesCountByMainCategory, generateCategoryStaticPaths } from '@/lib/man-pages-utils'; +import { + getSubCategoriesByMainCategoryPaginated, + getTotalSubCategoriesManPagesCount, +} from 'db/man_pages/man-pages-utils'; +import { formatName } from '@/lib/utils'; -export async function getStaticPaths() { - return generateCategoryStaticPaths(); -} +export const prerender = false; const { category } = Astro.params; if (!category) { - throw new Error('Category parameter is required'); + return Astro.redirect('/freedevtools/man-pages/'); } // Efficient pagination for page 1 @@ -22,19 +24,27 @@ const itemsPerPage = 12; const currentPage = 1; const offset = (currentPage - 1) * itemsPerPage; // 0 for page 1 -// Get ONLY the subcategories for page 1 from database (efficient!) -const currentPageSubcategories = getSubCategoriesByMainCategoryPaginated(category, itemsPerPage, offset); -const totalSubcategoriesCount = getSubCategoriesCountByMainCategory(category); -const totalManPagesInCategory = getTotalManPagesCountByMainCategory(category); -const totalPages = Math.ceil(totalSubcategoriesCount / itemsPerPage); + +const subcategories = await getSubCategoriesByMainCategoryPaginated( + category, + itemsPerPage, + offset +); +const {man_pages_count, sub_category_count} = await getTotalSubCategoriesManPagesCount(category); +const totalPages = Math.ceil(sub_category_count / itemsPerPage); const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Man Pages', href: '/freedevtools/man-pages/' }, - { label: category.replace('-', ' ').charAt(0).toUpperCase() + category.replace('-', ' ').slice(1) }, + { + label: + formatName(category), + }, ]; -const categoryTitle = category.replace('-', ' ').charAt(0).toUpperCase() + category.replace('-', ' ').slice(1); +const categoryTitle = + category.replace('-', ' ').charAt(0).toUpperCase() + + category.replace('-', ' ').slice(1); ---
-
{totalSubcategoriesCount >= 1000 ? Math.round(totalSubcategoriesCount / 1000) + 'k' : totalSubcategoriesCount}
-
Subcategories
+
+ { + sub_category_count >= 1000 + ? Math.round(sub_category_count / 1000) + 'k' + : sub_category_count + } +
+
+ Subcategories +
-
{totalManPagesInCategory >= 1000 ? Math.round(totalManPagesInCategory / 1000) + 'k' : totalManPagesInCategory}
+
+ { + man_pages_count >= 1000 + ? Math.round(man_pages_count / 1000) + 'k' + : man_pages_count + } +
Man Pages
@@ -79,33 +103,36 @@ const categoryTitle = category.replace('-', ' ').charAt(0).toUpperCase() + categ class="bg-slate-50 dark:bg-slate-800 rounded-lg p-4 mb-6 mt-8" >
- Showing {currentPageSubcategories.length} of {totalSubcategoriesCount} subcategories (Page {currentPage} of {totalPages}) + Showing {subcategories.length} of {sub_category_count} subcategories + (Page {currentPage} of {totalPages})
@@ -117,7 +144,9 @@ const categoryTitle = category.replace('-', ' ').charAt(0).toUpperCase() + categ /> -
- + \ No newline at end of file diff --git a/frontend/src/pages/man-pages/index.astro b/frontend/src/pages/man-pages/index.astro index 8f05bedda2..b6183bc6e2 100644 --- a/frontend/src/pages/man-pages/index.astro +++ b/frontend/src/pages/man-pages/index.astro @@ -4,13 +4,15 @@ import CreditsButton from '@/components/buttons/CreditsButton'; import ToolContainer from '@/components/tool/ToolContainer'; import ToolHead from '@/components/tool/ToolHead'; import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getManPageCategories, getOverview } from '@/lib/man-pages-utils'; +import { getManPageCategories, getOverview } from 'db/man_pages/man-pages-utils'; + +export const prerender = false; // Get categories from database -const categories = getManPageCategories(); +const categories = await getManPageCategories(); // Get overview stats -const overview = getOverview(); +const overview = await getOverview(); const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, @@ -24,7 +26,13 @@ const breadcrumbItems = [ path="/freedevtools/man-pages/" description="Browse and search manual pages (man pages) with detailed documentation for system calls, commands, and configuration files. Interactive man page viewer." canonical="https://hexmos.com/freedevtools/man-pages/" - keywords={['man pages', 'manual', 'documentation', 'system calls', 'commands']} + keywords={[ + 'man pages', + 'manual', + 'documentation', + 'system calls', + 'commands', + ]} ogImage="https://hexmos.com/freedevtools/tool-banners/man-pages-banner.png" twitterImage="https://hexmos.com/freedevtools/tool-banners/man-pages-banner.png" datePublished="2025-01-30" @@ -50,12 +58,22 @@ const breadcrumbItems = [
-
{categories.length >= 1000 ? Math.round(categories.length / 1000) + 'k' : categories.length}
+
+ { + categories.length >= 1000 + ? Math.round(categories.length / 1000) + 'k' + : categories.length + } +
Categories
- {(overview?.total_count || 0) >= 1000 ? Math.round((overview?.total_count || 0) / 1000) + 'k' : (overview?.total_count || 0)} + { + (overview?.total_count || 0) >= 1000 + ? Math.round((overview?.total_count || 0) / 1000) + 'k' + : overview?.total_count || 0 + }
Man Pages
@@ -65,32 +83,39 @@ const breadcrumbItems = [
-
+
- \ No newline at end of file + diff --git a/frontend/src/pages/man-pages/sitemap-[index].xml.ts b/frontend/src/pages/man-pages/sitemap-[index].xml.ts index f1bd8bbcd9..18b2bd99e6 100644 --- a/frontend/src/pages/man-pages/sitemap-[index].xml.ts +++ b/frontend/src/pages/man-pages/sitemap-[index].xml.ts @@ -1,148 +1,57 @@ +import { getAllManPagesPaginated } from 'db/man_pages/man-pages-utils'; import type { APIRoute } from 'astro'; -// maximum URLs per sitemap file const MAX_URLS = 5000; -export async function getStaticPaths() { - const Database = (await import('better-sqlite3')).default; - const path = (await import('path')).default; +export const prerender = false; - // Loader function for sitemap URLs - async function loadUrls() { - const dbPath = path.join(process.cwd(), 'db/all_dbs/man-pages-db.db'); - const db = new Database(dbPath, { readonly: true }); - const now = new Date().toISOString(); - - // Build URLs with placeholder for site - const stmt = db.prepare(` - SELECT main_category, sub_category, slug - FROM man_pages - WHERE slug IS NOT NULL AND slug != '' - ORDER BY main_category, sub_category, slug - `); - - const manPages = stmt.all() as Array<{ - main_category: string; - sub_category: string; - slug: string; - }>; - - const urls = manPages.map((manPage) => { - return ` - - __SITE__/man-pages/${manPage.main_category}/${manPage.sub_category}/${manPage.slug}/ - ${now} - daily - 0.8 - `; - }); - - // Include landing page - urls.unshift(` - - __SITE__/man-pages/ - ${now} - daily - 0.9 - `); - - // Add category index pages - const categoryStmt = db.prepare(` - SELECT DISTINCT main_category - FROM man_pages - ORDER BY main_category - `); - const categories = categoryStmt.all() as Array<{ main_category: string }>; - - categories.forEach(({ main_category }) => { - urls.push(` - - __SITE__/man-pages/${main_category}/ - ${now} - daily - 0.7 - `); - }); - - // Add subcategory index pages - const subcategoryStmt = db.prepare(` - SELECT DISTINCT main_category, sub_category - FROM man_pages - ORDER BY main_category, sub_category - `); - const subcategories = subcategoryStmt.all() as Array<{ - main_category: string; - sub_category: string; - }>; - - subcategories.forEach(({ main_category, sub_category }) => { - urls.push(` - - __SITE__/man-pages/${main_category}/${sub_category}/ - ${now} - daily - 0.6 - `); - }); +function escapeXml(unsafe: string): string { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} - db.close(); - return urls; +export const GET: APIRoute = async ({ params, site }) => { + const index = parseInt(params.index || '1', 10); + if (isNaN(index) || index < 1) { + return new Response('Invalid index', { status: 400 }); } - // Pre-count total pages - try { - const Database = (await import('better-sqlite3')).default; - const path = (await import('path')).default; - const dbPath = path.join(process.cwd(), 'db/all_dbs/man-pages-db.db'); - const db = new Database(dbPath, { readonly: true }); + const offset = (index - 1) * MAX_URLS; + const limit = MAX_URLS; - const countStmt = db.prepare(` - SELECT - (SELECT COUNT(*) FROM man_pages WHERE slug IS NOT NULL AND slug != '') + - (SELECT COUNT(DISTINCT main_category) FROM man_pages) + - (SELECT COUNT(DISTINCT main_category || '-' || sub_category) FROM man_pages) + - 1 as total - `); - const result = countStmt.get() as { total: number }; - const totalUrls = result.total; - const totalPages = Math.ceil(totalUrls / MAX_URLS); + const manPages = await getAllManPagesPaginated(limit, offset); - db.close(); - - return Array.from({ length: totalPages }, (_, i) => ({ - params: { index: String(i + 1) }, - props: { loadUrls }, // pass only the function reference - })); - } catch (error) { - console.error('Error counting man pages for sitemap:', error); - // Fallback to single page - return [{ params: { index: '1' }, props: { loadUrls: async () => [] } }]; + if (!manPages || manPages.length === 0) { + return new Response('Not Found', { status: 404 }); } -} -export const GET: APIRoute = async ({ site, params, props }) => { - const loadUrls: () => Promise = props.loadUrls; - let urls = await loadUrls(); + const now = new Date().toISOString(); - // Replace placeholder with actual site - urls = urls.map((u) => u.replace(/__SITE__/g, site?.toString() || '')); - - // Split into chunks - const sitemapChunks: string[][] = []; - for (let i = 0; i < urls.length; i += MAX_URLS) { - sitemapChunks.push(urls.slice(i, i + MAX_URLS)); - } + const siteUrl = site?.toString().replace(/\/$/, '') || 'https://hexmos.com/freedevtools'; - const index = parseInt(params.index || '1', 10) - 1; - const chunk = sitemapChunks[index]; + const urls = manPages.map((page) => { + const escapedCategory = escapeXml(page.main_category); + const escapedSubCategory = escapeXml(page.sub_category); + const escapedSlug = escapeXml(page.slug); - if (!chunk) return new Response('Not Found', { status: 404 }); + return ` + + ${siteUrl}/man-pages/${escapedCategory}/${escapedSubCategory}/${escapedSlug}/ + ${now} + monthly + 0.8 + `; + }); const xml = ` - - - ${chunk.join('\n')} -`; + + + ${urls.join('\n')} + `; return new Response(xml, { headers: { @@ -151,3 +60,8 @@ export const GET: APIRoute = async ({ site, params, props }) => { }, }); }; + +export function getStaticPaths() { + return []; +} + diff --git a/frontend/src/pages/man-pages/sitemap.xml.ts b/frontend/src/pages/man-pages/sitemap.xml.ts index d400c486a7..28a83a3a4a 100644 --- a/frontend/src/pages/man-pages/sitemap.xml.ts +++ b/frontend/src/pages/man-pages/sitemap.xml.ts @@ -1,143 +1,41 @@ +import { getOverview } from 'db/man_pages/man-pages-utils'; import type { APIRoute } from 'astro'; -export const GET: APIRoute = async ({ site, params }) => { - const Database = (await import('better-sqlite3')).default; - const path = (await import('path')).default; +const MAX_URLS = 5000; - const now = new Date().toISOString(); - const MAX_URLS = 5000; - - try { - // Connect to database - const dbPath = path.join(process.cwd(), 'db/all_dbs/man-pages-db.db'); - const db = new Database(dbPath, { readonly: true }); - - // Get all man pages from database - const stmt = db.prepare(` - SELECT main_category, sub_category, slug - FROM man_pages - WHERE slug IS NOT NULL AND slug != '' - ORDER BY main_category, sub_category, slug - `); - - const manPages = stmt.all() as Array<{ - main_category: string; - sub_category: string; - slug: string; - }>; - - // Map man pages to sitemap URLs - const urls = manPages.map((manPage) => { - return ` - - ${site}/man-pages/${manPage.main_category}/${manPage.sub_category}/${manPage.slug}/ - ${now} - daily - 0.8 - `; - }); - - // Include the main landing pages - urls.unshift(` - - ${site}/man-pages/ - ${now} - daily - 0.9 - `); - - // Add category index pages - const categoryStmt = db.prepare(` - SELECT DISTINCT main_category - FROM man_pages - ORDER BY main_category - `); - const categories = categoryStmt.all() as Array<{ main_category: string }>; - - categories.forEach(({ main_category }) => { - urls.push(` - - ${site}/man-pages/${main_category}/ - ${now} - daily - 0.7 - `); - }); - - // Add subcategory index pages - const subcategoryStmt = db.prepare(` - SELECT DISTINCT main_category, sub_category - FROM man_pages - ORDER BY main_category, sub_category - `); - const subcategories = subcategoryStmt.all() as Array<{ - main_category: string; - sub_category: string; - }>; +export const prerender = false; - subcategories.forEach(({ main_category, sub_category }) => { - urls.push(` - - ${site}/man-pages/${main_category}/${sub_category}/ - ${now} - daily - 0.6 - `); - }); - - db.close(); - - // Split URLs into chunks of MAX_URLS - const sitemapChunks: string[][] = []; - for (let i = 0; i < urls.length; i += MAX_URLS) { - sitemapChunks.push(urls.slice(i, i + MAX_URLS)); - } - - // If ?index param exists, serve a chunked sitemap - if (params?.index) { - const index = parseInt(params.index, 10) - 1; // 1-based: /sitemap-1.xml - const chunk = sitemapChunks[index]; - - if (!chunk) return new Response('Not Found', { status: 404 }); - - const xml = ` - - - ${chunk.join('\n')} -`; - - return new Response(xml, { - headers: { - 'Content-Type': 'application/xml', - 'Cache-Control': 'public, max-age=3600', - }, - }); - } +export const GET: APIRoute = async ({ site }) => { + const now = new Date().toISOString(); - // Return sitemap index - const indexXml = ` - + const overview = await getOverview(); + const totalCount = overview?.total_count || 0; - - ${sitemapChunks - .map( - (_, i) => ` - - ${site}/man-pages/sitemap-${i + 1}.xml - ${now} - ` - ) - .join('\n')} -`; + const totalSitemaps = Math.ceil(totalCount / MAX_URLS); - return new Response(indexXml, { - headers: { - 'Content-Type': 'application/xml', - 'Cache-Control': 'public, max-age=3600', - }, - }); - } catch (error) { - console.error('Error generating man pages sitemap:', error); - return new Response('Internal Server Error', { status: 500 }); + const sitemapUrls: string[] = []; + for (let i = 1; i <= totalSitemaps; i++) { + sitemapUrls.push(`${site}/man-pages/sitemap-${i}.xml`); } + + const indexXml = ` + + + ${sitemapUrls + .map( + (url) => ` + + ${url} + ${now} + ` + ) + .join('\n')} + `; + + return new Response(indexXml, { + headers: { + 'Content-Type': 'application/xml', + 'Cache-Control': 'public, max-age=3600', + }, + }); }; diff --git a/frontend/src/pages/man-pages_pages/sitemap-[index].xml.ts b/frontend/src/pages/man-pages_pages/sitemap-[index].xml.ts new file mode 100644 index 0000000000..b2560d3c67 --- /dev/null +++ b/frontend/src/pages/man-pages_pages/sitemap-[index].xml.ts @@ -0,0 +1,147 @@ +// import { +// getManPageCategories, +// getSubCategoriesCountByMainCategory, +// getSubCategoriesByMainCategory, +// getManPagesCountInSubCategory, +// } from 'db/man_pages/man-pages-utils'; +// import type { APIRoute } from 'astro'; + +// const MAX_URLS = 5000; +// const PRODUCTION_SITE = 'https://hexmos.com/freedevtools'; + +// // Escape XML special characters +// function escapeXml(unsafe: string): string { +// return unsafe +// .replace(/&/g, '&') +// .replace(//g, '>') +// .replace(/"/g, '"') +// .replace(/'/g, '''); +// } + +// // Loader function for sitemap URLs +// async function loadUrls() { +// const now = new Date().toISOString(); +// const urls: string[] = []; + +// // Root man-pages page +// urls.push( +// ` +// __SITE__/man-pages/ +// ${now} +// daily +// 0.9 +// ` +// ); + +// // Get all categories +// const categories = await getManPageCategories(); + +// // Category pagination (12 items per page) +// const categoryItemsPerPage = 12; +// for (const { category: main_category } of categories) { +// // Get subcategories count for this category +// const subcategoryCount = await getSubCategoriesCountByMainCategory(main_category); +// const totalCategoryPages = Math.ceil( +// subcategoryCount / categoryItemsPerPage +// ); + +// // Add category index page (page 1 is the same as the category root) +// const escapedCategory = escapeXml(main_category); +// urls.push( +// ` +// __SITE__/man-pages/${escapedCategory}/ +// ${now} +// daily +// 0.7 +// ` +// ); + +// // Pagination pages for category (skip page 1 as it's the same as the root) +// for (let i = 2; i <= totalCategoryPages; i++) { +// urls.push( +// ` +// __SITE__/man-pages/${escapedCategory}/${i}/ +// ${now} +// daily +// 0.6 +// ` +// ); +// } + +// // Get all subcategories for this category +// const subcategories = await getSubCategoriesByMainCategory(main_category); + +// // Subcategory pagination (20 items per page) +// const subcategoryItemsPerPage = 20; +// for (const { name: sub_category } of subcategories) { +// // Get man pages count for this subcategory +// const manPagesCount = await getManPagesCountInSubCategory(main_category, sub_category); +// const totalSubcategoryPages = Math.ceil( +// manPagesCount / subcategoryItemsPerPage +// ); + +// // Add subcategory index page (page 1 is the same as the subcategory root) +// const escapedSubCategory = escapeXml(sub_category); +// urls.push( +// ` +// __SITE__/man-pages/${escapedCategory}/${escapedSubCategory}/ +// ${now} +// daily +// 0.6 +// ` +// ); + +// // Pagination pages for subcategory (skip page 1 as it's the same as the root) +// for (let i = 2; i <= totalSubcategoryPages; i++) { +// urls.push( +// ` +// __SITE__/man-pages/${escapedCategory}/${escapedSubCategory}/${i}/ +// ${now} +// daily +// 0.5 +// ` +// ); +// } +// } +// } + +// return urls; +// } + +// export const prerender = false; + +// export const GET: APIRoute = async ({ site, params }) => { +// // SSR mode: call loadUrls directly +// let urls = await loadUrls(); + +// // Replace placeholder with actual site - use production site if localhost or undefined +// const siteUrl = +// site && !String(site).includes('localhost') +// ? String(site) +// : PRODUCTION_SITE; +// urls = urls.map((u) => u.replace(/__SITE__/g, siteUrl)); + +// // Split into chunks +// const sitemapChunks: string[][] = []; +// for (let i = 0; i < urls.length; i += MAX_URLS) { +// sitemapChunks.push(urls.slice(i, i + MAX_URLS)); +// } + +// const index = parseInt(params?.index || '1', 10) - 1; +// const chunk = sitemapChunks[index]; + +// if (!chunk) return new Response('Not Found', { status: 404 }); + +// const xml = ` +// +// ${chunk.join('\n')} +// `; + +// return new Response(xml, { +// headers: { +// 'Content-Type': 'application/xml', +// 'Cache-Control': 'public, max-age=3600', +// }, +// }); +// }; diff --git a/frontend/src/pages/man-pages_pages/sitemap.xml.ts b/frontend/src/pages/man-pages_pages/sitemap.xml.ts new file mode 100644 index 0000000000..462d5787a9 --- /dev/null +++ b/frontend/src/pages/man-pages_pages/sitemap.xml.ts @@ -0,0 +1,159 @@ +// import { +// getManPageCategories, +// getSubCategories, +// getSubCategoriesCountByMainCategory, +// getManPagesCountInSubCategory, +// } from 'db/man_pages/man-pages-utils'; +// import type { APIRoute } from 'astro'; + +// const PRODUCTION_SITE = 'https://hexmos.com/freedevtools'; +// const MAX_URLS = 5000; + +// // Escape XML special characters +// function escapeXml(unsafe: string): string { +// return unsafe +// .replace(/&/g, '&') +// .replace(//g, '>') +// .replace(/"/g, '"') +// .replace(/'/g, '''); +// } + +// export const prerender = false; + +// export const GET: APIRoute = async ({ site }) => { +// const now = new Date().toISOString(); + +// // Use production site if localhost or undefined +// const siteUrl = +// site && !String(site).includes('localhost') +// ? String(site) +// : PRODUCTION_SITE; + +// const urls: string[] = []; + +// // Root man-pages page +// urls.push( +// ` +// ${siteUrl}/man-pages/ +// ${now} +// daily +// 0.9 +// ` +// ); + +// // Get all categories +// const categories = await getManPageCategories(); + +// // Category pagination (12 items per page) +// const categoryItemsPerPage = 12; +// for (const { category: main_category } of categories) { +// // Get subcategories count for this category +// const subcategoryCount = await getSubCategoriesCountByMainCategory(main_category); +// const totalCategoryPages = Math.ceil( +// subcategoryCount / categoryItemsPerPage +// ); + +// // Add category index page (page 1 is the same as the category root) +// const escapedCategory = escapeXml(main_category); +// urls.push( +// ` +// ${siteUrl}/man-pages/${escapedCategory}/ +// ${now} +// daily +// 0.7 +// ` +// ); + +// // Pagination pages for category (skip page 1 as it's the same as the root) +// for (let i = 2; i <= totalCategoryPages; i++) { +// urls.push( +// ` +// ${siteUrl}/man-pages/${escapedCategory}/${i}/ +// ${now} +// daily +// 0.6 +// ` +// ); +// } + +// // Get all subcategories for this category +// // Note: getSubCategoriesByMainCategory returns all subcategories +// const { getSubCategoriesByMainCategory } = await import('db/man_pages/man-pages-utils'); +// const subcategories = await getSubCategoriesByMainCategory(main_category); + +// // Subcategory pagination (20 items per page) +// const subcategoryItemsPerPage = 20; +// for (const { name: sub_category } of subcategories) { +// // Get man pages count for this subcategory +// const manPagesCount = await getManPagesCountInSubCategory(main_category, sub_category); +// const totalSubcategoryPages = Math.ceil( +// manPagesCount / subcategoryItemsPerPage +// ); + +// // Add subcategory index page (page 1 is the same as the subcategory root) +// const escapedSubCategory = escapeXml(sub_category); +// urls.push( +// ` +// ${siteUrl}/man-pages/${escapedCategory}/${escapedSubCategory}/ +// ${now} +// daily +// 0.6 +// ` +// ); + +// // Pagination pages for subcategory (skip page 1 as it's the same as the root) +// for (let i = 2; i <= totalSubcategoryPages; i++) { +// urls.push( +// ` +// ${siteUrl}/man-pages/${escapedCategory}/${escapedSubCategory}/${i}/ +// ${now} +// daily +// 0.5 +// ` +// ); +// } +// } +// } + +// // If total URLs <= MAX_URLS, return the single sitemap +// if (urls.length <= MAX_URLS) { +// const xml = ` +// +// ${urls.join('\n')} +// `; + +// return new Response(xml, { +// headers: { +// 'Content-Type': 'application/xml', +// 'Cache-Control': 'public, max-age=3600', +// }, +// }); +// } + +// // Otherwise, split URLs into chunks and return a sitemap index +// const sitemapChunks: string[][] = []; +// for (let i = 0; i < urls.length; i += MAX_URLS) { +// sitemapChunks.push(urls.slice(i, i + MAX_URLS)); +// } + +// const indexXml = ` +// +// ${sitemapChunks +// .map( +// (_, i) => ` +// +// ${siteUrl}/man-pages_pages/sitemap-${i + 1}.xml +// ${now} +// ` +// ) +// .join('\n')} +// `; + +// return new Response(indexXml, { +// headers: { +// 'Content-Type': 'application/xml', +// 'Cache-Control': 'public, max-age=3600', +// }, +// }); +// }; diff --git a/frontend/src/pages/mcp-pages/sitemap.xml.ts b/frontend/src/pages/mcp-pages/sitemap.xml.ts index b182d54477..7993c1e9cd 100644 --- a/frontend/src/pages/mcp-pages/sitemap.xml.ts +++ b/frontend/src/pages/mcp-pages/sitemap.xml.ts @@ -1,39 +1,22 @@ import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; +import { getOverview, getAllMcpCategories } from 'db/mcp/mcp-utils'; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); - // Get all categories from the metadata - const metadataEntries = await getCollection('mcpMetadata' as any); - const metadata = (metadataEntries[0] as any)?.data; + // Get overview for main pagination + const { totalCategoryCount } = await getOverview(); - if (!metadata) { - return new Response('Metadata not found', { status: 500 }); - } - - // Get all categories from the metadata - const categories = Object.keys(metadata.categories); + // Get all categories for category pagination + const categories = await getAllMcpCategories(1, 100); - // Calculate total pages for MCP directory pagination - const totalCategories = categories.length; + const urls: string[] = []; const itemsPerPage = 30; - const totalPages = Math.ceil(totalCategories / itemsPerPage); - const urls: string[] = []; + // 1. Main MCP Directory Pagination (/mcp/1/, /mcp/2/, etc.) + const totalMainPages = Math.ceil(totalCategoryCount / itemsPerPage); - // Root MCP page - urls.push( - ` - ${site}/mcp/ - ${now} - daily - 0.9 - ` - ); - - // Pagination pages - for (let i = 1; i <= totalPages; i++) { + for (let i = 1; i <= totalMainPages; i++) { urls.push( ` ${site}/mcp/${i}/ @@ -44,6 +27,22 @@ export const GET: APIRoute = async ({ site }) => { ); } + // 2. Category Pagination (/mcp/[category]/1/, /mcp/[category]/2/, etc.) + for (const category of categories) { + const totalCategoryPages = Math.ceil(category.count / itemsPerPage); + + for (let i = 1; i <= totalCategoryPages; i++) { + urls.push( + ` + ${site}/mcp/${category.slug}/${i}/ + ${now} + daily + 0.8 + ` + ); + } + } + const xml = ` diff --git a/frontend/src/pages/mcp/[category]/[page].astro b/frontend/src/pages/mcp/[category]/[page].astro deleted file mode 100644 index 89b2719a74..0000000000 --- a/frontend/src/pages/mcp/[category]/[page].astro +++ /dev/null @@ -1,145 +0,0 @@ ---- -import CreditsButton from '@/components/buttons/CreditsButton'; -import ToolContainer from '@/components/tool/ToolContainer'; -import ToolHead from '@/components/tool/ToolHead'; -import BaseLayout from '@/layouts/BaseLayout.astro'; -import { generateMcpCategoryPaginatedPaths } from '@/lib/mcp-utils'; -import { formatRepositoryName } from '@/lib/utils'; -import { getEntry } from 'astro:content'; -import AdBanner from '../../../components/banner/AdBanner.astro'; -import Pagination from '../../../components/PaginationComponent.astro'; -import RepositoryCard from '../_McpRepoCard.astro'; - -// Generate static paths for all MCP categories with pagination -export async function getStaticPaths({ paginate }) { - return await generateMcpCategoryPaginatedPaths({ paginate }); -} - -// Get category and page from params -const { category } = Astro.params; -const { page } = Astro.props; - -if (!category) { - throw new Error('Category parameter is required'); -} - -// Get category data -const categoryEntry = await getEntry('mcpCategoryData', category); - -if (!categoryEntry) { - throw new Error(`Category '${category}' not found`); -} - -const categoryData = categoryEntry.data; -const categoryName = categoryData.categoryDisplay; -const categoryDescription = categoryData.description || ''; - -// SEO data -const title = `${categoryName} MCP Servers & Repositories – ${page.total} Model Context Protocol Tools (Page ${page.currentPage} of ${page.lastPage}) | Free DevTools by Hexmos`; - -const description = `Discover ${page.total} ${categoryName} MCP servers and repositories for Model Context Protocol integrations. Browse tools compatible with Claude, Cursor, and Windsurf — free, open source, and easy to explore.`; - -const keywords = [ - 'MCP', - 'Model Context Protocol', - categoryName, - 'MCP servers', - 'AI tools', - 'developer tools', - 'open source', - 'repositories', - 'pagination', -]; - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, - { label: categoryName, href: `/freedevtools/mcp/${category}/1/` }, -]; ---- - - - -
- -
- - - -
-
- Showing {page.data.length} of {page.total} repositories (Page { - page.currentPage - } of {page.lastPage}) -
-
- - -
- { - page.data.map((server) => { - const formattedName = formatRepositoryName(server.name); - // Use the repositoryId that was added to the server object - const repositoryId = server.repositoryId; - return ( - - ); - }) - } -
- - - - - - -
-
diff --git a/frontend/src/pages/mcp/[category]/[repositoryId].astro b/frontend/src/pages/mcp/[category]/[repositoryId].astro deleted file mode 100644 index af2d4385df..0000000000 --- a/frontend/src/pages/mcp/[category]/[repositoryId].astro +++ /dev/null @@ -1,444 +0,0 @@ ---- -import { getEntry } from 'astro:content'; -import { marked } from 'marked'; -import AdBanner from '../../../components/banner/AdBanner.astro'; -import CreditsButton from '../../../components/buttons/CreditsButton'; -import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; -import ToolContainer from '../../../components/tool/ToolContainer'; -import ToolHead from '../../../components/tool/ToolHead'; -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { generateMcpStaticPaths } from '../../../lib/mcp-utils'; -import { formatRepositoryName } from '../../../lib/utils'; -import Banner from '../../../components/banner/BannerIndex.astro'; - -function processMcpReadmeLinks(html: string): string { - // First, add IDs to all headings (h1-h6) so anchor links work - html = html.replace( - /<(h[1-6])([^>]*)>([^<]+)<\/h[1-6]>/gi, - (match, tag, attributes, content) => { - // Generate anchor ID from heading content - const anchorId = content - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') // Remove special chars - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Replace multiple hyphens with single - .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens - - // Add scroll margin to offset the header - use both class and inline style - const existingClass = attributes.includes('class=') ? attributes : ''; - const scrollMarginClass = existingClass - ? existingClass.replace(/class="([^"]*)"/, 'class="$1 scroll-mt-32"') - : 'class="scroll-mt-32"'; - - // Add inline style as backup to ensure scroll margin works - const existingStyle = attributes.includes('style=') ? attributes : ''; - const scrollMarginStyle = existingStyle - ? existingStyle.replace( - /style="([^"]*)"/, - 'style="$1; scroll-margin-top: 8rem;"' - ) - : 'style="scroll-margin-top: 8rem;"'; - - return `<${tag} ${scrollMarginClass} ${scrollMarginStyle} id="${anchorId}">${content}`; - } - ); - - // Then process the links - return html.replace( - /]*?)href="([^"]*?)"([^>]*?)>/g, - ( - match: string, - beforeHref: string, - href: string, - afterHref: string - ): string => { - // Keep absolute URLs (https/http) as clickable links - if (/^https?:\/\//i.test(href)) { - // Add target="_blank" for external links - if (!match.includes('target=')) { - return ``; - } - return match; - } - - // Handle anchor links (starting with #) - keep them as clickable for scrolling - if (href.startsWith('#')) { - return match; - } - - // Disable all relative links (/, ../, ./, etc.) by removing href and adding disabled styling - return ``; - } - ); -} - -// Generate static paths for all MCP repositories using optimized utility -export async function getStaticPaths() { - return await generateMcpStaticPaths(); -} - -// Get parameters -const { category, repositoryId } = Astro.params; - -if (!category || !repositoryId) { - throw new Error('Category and repositoryId parameters are required'); -} - -// Use getEntry for direct access instead of getCollection + find -// This is more efficient as it directly accesses the specific category file -const categoryEntry = await getEntry('mcpCategoryData', category); - -if (!categoryEntry) { - throw new Error(`Category '${category}' not found`); -} - -const categoryData = categoryEntry.data; -const categoryName = categoryData.categoryDisplay; - -// Find the specific repository -const server = categoryData.repositories[repositoryId]; -if (!server) { - throw new Error( - `Repository '${repositoryId}' not found in category '${category}'` - ); -} - -// Format repository name -const formattedName = formatRepositoryName(server.name); - -// Process README content -let processedReadmeContent: string = ''; -if (server.readme_content) { - try { - // Convert markdown to HTML - let htmlContent = await marked(server.readme_content); - - // Process links: keep absolute URLs and anchor links, disable relative links - processedReadmeContent = processMcpReadmeLinks(htmlContent); - } catch (error) { - console.warn('Error processing README content:', error); - processedReadmeContent = server.readme_content; - } -} - -// Calculate stats -const stats = { - githubStars: server.stars, - weeklyDownloads: server.npm_downloads || 0, - tools: 1, // This would need to be calculated from actual data - lastUpdated: new Date(server.updated_at).toLocaleDateString(), -}; - -// SEO data -const title = `${formattedName} – ${categoryName} MCP Server by ${server.owner.charAt(0).toUpperCase() + server.owner.slice(1)} Model Context Protocol Tool | Free DevTools by Hexmos`; - -const description = - server.description || - `${server.owner.charAt(0).toUpperCase() + server.owner.slice(1)}'s ${formattedName} MCP server helps your AI generate more accurate and context-aware responses. Supported in Copilot Agent, Cursor, Claude Code, Windsurf, and Cline – free, open source, and ready to integrate.`; - -let keywords = ['MCP', 'Model Context Protocol', formattedName, categoryName]; - -if (server.keywords && server.keywords.length > 0) { - keywords = [...keywords, ...server.keywords]; -} - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, - { label: categoryName, href: `/freedevtools/mcp/${category}/1/` }, - { label: formattedName }, -]; ---- - - - -
- -
- - -
- - - - -
- { - server.readme_content ? ( -
-
-
-
-
- ) : ( -
-
📝
-

No documentation available

-

- This repository doesn't have README content available yet. -

-
- ) - } -
-
- - -
- -
- - - - - diff --git a/frontend/src/pages/mcp/[category]/[slug].astro b/frontend/src/pages/mcp/[category]/[slug].astro new file mode 100644 index 0000000000..fc80475f9f --- /dev/null +++ b/frontend/src/pages/mcp/[category]/[slug].astro @@ -0,0 +1,27 @@ +--- +import PaginationView from './_Pagination.astro'; +import PageView from './_Page.astro'; + +export const prerender = false; + +const { category, slug } = Astro.params; + +// Early return if missing params +if (!category || !slug) { + return new Response(null, { status: 404 }); +} + +const requestStartTime = Date.now(); +console.log(`[${new Date().toISOString()}] Request reached server: /freedevtools/mcp/${category}/${slug}/`); + +// Check if slug is numeric (pagination) or a string (repository) +const isNumericPage = /^\d+$/.test(slug); + +console.log(`[${new Date().toISOString()}] Total request time for /freedevtools/mcp/${category}/${slug}/: ${Date.now() - requestStartTime}ms`); +--- + +{isNumericPage ? ( + +) : ( + +)} diff --git a/frontend/src/pages/mcp/[category]/_Page.astro b/frontend/src/pages/mcp/[category]/_Page.astro new file mode 100644 index 0000000000..4f5dcc2148 --- /dev/null +++ b/frontend/src/pages/mcp/[category]/_Page.astro @@ -0,0 +1,362 @@ +--- +import BaseLayout from '@/layouts/BaseLayout.astro'; +import ToolContainer from '@/components/tool/ToolContainer'; +import AdBanner from '@/components/banner/AdBanner.astro'; +import ToolHead from '@/components/tool/ToolHead'; +import SeeAlsoIndex from '@/components/seealso/SeeAlsoIndex.astro'; +import Banner from '@/components/banner/BannerIndex.astro'; +import CreditsButton from '@/components/buttons/CreditsButton'; +import { formatName } from '@/lib/utils'; +import { + getMcpPage, + hashUrlToKey +} from 'db/mcp/mcp-utils'; + +const { category, slug } = Astro.props; + +// Handle repository route: /mcp/[category]/[repositoryId] +const hashId = await hashUrlToKey(category, slug); +const repoPage = await getMcpPage(hashId); + +if (!repoPage) { + return new Response(null, { status: 404 }); +} + +const categoryName = formatName(category); + +// Format repository name +const formattedName = formatName(repoPage.name); + + +const processedReadmeContent = repoPage.readme_content || ''; + +// Calculate stats +const stats = { + githubStars: repoPage.stars, + weeklyDownloads: repoPage.npm_downloads || 0, + tools: 1, + lastUpdated: new Date(repoPage.updated_at).toLocaleDateString(), +}; +const repoOwner=repoPage.owner.charAt(0).toUpperCase() + repoPage.owner.slice(1) + +// SEO data +const title = `${formattedName} – ${categoryName} MCP Server by ${repoOwner} Model Context Protocol Tool | Free DevTools by Hexmos`; +const description = + repoPage.description || + `${repoOwner}'s ${formattedName} MCP server helps your AI generate more accurate and context-aware responses. Supported in Copilot Agent, Cursor, Claude Code, Windsurf, and Cline – free, open source, and ready to integrate.`; + +let keywords = repoPage?.keywords; + +// Breadcrumb data +const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, + { label: categoryName, href: `/freedevtools/mcp/${category}/1/` }, + { label: formattedName }, +]; + +const repositoryData = { + category, + categoryName, + repositoryId: slug, + formattedName, + processedReadmeContent, + stats, + breadcrumbItems, + title, + description, + keywords, + url: repoPage.url, + license: repoPage.license, + owner: repoPage.owner, + imageUrl: repoPage.image_url, + npm_url: repoPage.npm_url, +}; +--- + + + +
+ +
+ + +
+ +
+ +
+

Author

+
+
+ {repositoryData.imageUrl ? ( + {`${repositoryData.formattedName} + ) : ( +
+ MCP Server +
+ )} +
+
+

+ {repositoryData.owner || 'Unknown Author'} +

+
+ + + + {repositoryData.license} +
+
+
+
+ + +
+

Quick Info

+
+
+ + GitHub + GitHub Stars + + + + + + {repositoryData.stats.githubStars} + +
+
+ + NPM + Weekly Downloads + + + + + + {repositoryData.stats.weeklyDownloads} + +
+
+ + + + + + Tools + + {repositoryData.stats.tools} +
+
+ + + + + Last Updated + + {repositoryData.stats.lastUpdated} +
+
+
+ + +
+

Actions

+
+ + GitHub + View on GitHub + + {repositoryData.npm_url && ( + + NPM + View on NPM + + )} +
+
+ + + {repositoryData.keywords && + repositoryData.keywords.length > 0 && ( +
+

Tags

+
+ {repositoryData.keywords.map((keyword: any) => ( + + {keyword} + + ))} +
+
+ )} +
+ + +
+ {repositoryData.processedReadmeContent ? ( +
+
+
+
+
+ ) : ( +
+
📝
+

No documentation available

+

+ This repository doesn't have README content available yet. +

+
+ )} +
+
+ + +
+ +
+ + + + + diff --git a/frontend/src/pages/mcp/[category]/_Pagination.astro b/frontend/src/pages/mcp/[category]/_Pagination.astro new file mode 100644 index 0000000000..8897c2513c --- /dev/null +++ b/frontend/src/pages/mcp/[category]/_Pagination.astro @@ -0,0 +1,167 @@ +--- +import BaseLayout from '@/layouts/BaseLayout.astro'; +import ToolContainer from '@/components/tool/ToolContainer'; +import AdBanner from '@/components/banner/AdBanner.astro'; +import ToolHead from '@/components/tool/ToolHead'; +import RepositoryCard from '../_McpRepoCard.astro'; +import Pagination from '@/components/PaginationComponent.astro'; +import CreditsButton from '@/components/buttons/CreditsButton'; +import { formatName } from '@/lib/utils'; +import { + getMcpCategory, + getMcpPagesByCategory, +} from 'db/mcp/mcp-utils'; + +const requestStartTime = Date.now(); +console.log(`[${new Date().toISOString()}] Request reached server: /freedevtools/mcp/${Astro.props.category}/${Astro.props.page}/`); + +const { category, page } = Astro.props; +const pageNumber = parseInt(page, 10); + +// Get category data +const categoryData = await getMcpCategory(category); +if (!categoryData) { + return new Response(null, { status: 404 }); +} + +const categoryName = categoryData.name; +const categoryDescription = categoryData.description || ''; + +const itemsPerPage = 30; +const totalRepositories = categoryData.count; +const totalPages = Math.ceil(totalRepositories / itemsPerPage); + +// Validate page number +if (pageNumber > totalPages || pageNumber < 1) { + return new Response(null, { status: 404 }); +} + +// Get repositories for current page +const dbRepositories = await getMcpPagesByCategory(category, pageNumber, itemsPerPage); +console.log(dbRepositories.length); + +// SEO data +const title = `${categoryName} MCP Servers & Repositories – ${totalRepositories} Model Context Protocol Tools (Page ${pageNumber} of ${totalPages}) | Free DevTools by Hexmos`; +const description = `Discover ${totalRepositories} ${categoryName} MCP servers and repositories for Model Context Protocol integrations. Browse tools compatible with Claude, Cursor, and Windsurf — free, open source, and easy to explore.`; +const keywords = [ + 'MCP', + 'Model Context Protocol', + categoryName, + 'MCP servers', + 'AI tools', + 'developer tools', + 'open source', + 'repositories', + 'pagination', +]; + +// Breadcrumb data +const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, + { label: categoryName, href: `/freedevtools/mcp/${category}/1/` }, +]; + +const pageData = { + category, + categoryName, + categoryDescription, + currentPage: pageNumber, + totalPages, + totalRepositories, + repositories: dbRepositories, + breadcrumbItems, + title, + description, + keywords, +}; + +console.log(`[${new Date().toISOString()}] Total request time for /freedevtools/mcp/${category}/${page}/: ${Date.now() - requestStartTime}ms`); +--- + + + +
+ +
+ + + +
+
+ Showing {pageData.repositories.length} of {pageData.totalRepositories}{' '} + repositories (Page {pageData.currentPage} of {pageData.totalPages}) +
+
+ + +
+ {pageData.repositories.map((server: any) => { + const formattedName = formatName(server.name); + return ( + + ); + })} +
+ + + + + + +
+
diff --git a/frontend/src/pages/mcp/[category]/index.astro b/frontend/src/pages/mcp/[category]/index.astro index b0654e9396..1758cb6b70 100644 --- a/frontend/src/pages/mcp/[category]/index.astro +++ b/frontend/src/pages/mcp/[category]/index.astro @@ -1,21 +1,139 @@ --- -import { getCollection } from 'astro:content'; +import { + getAllMcpCategories, + getMcpCategory, + getOverview +} from 'db/mcp/mcp-utils'; +import BaseLayout from '@/layouts/BaseLayout.astro'; +import Mcp from '../_Mcp.astro'; -export async function getStaticPaths() { - const categoryEntries = await getCollection('mcpCategoryData'); - - return categoryEntries.map(entry => ({ - params: { category: entry.data.category } - })); -} +export const prerender = false; -// Redirect to page 1 of the category const { category } = Astro.params; if (!category) { - throw new Error('Category parameter is required'); + return new Response(null, { status: 404 }); +} + +let paginationData: any = null; +let shouldRedirect = false; +let redirectUrl = ''; + +// If category is numeric, this is actually a pagination route +// Render pagination content directly (workaround for route priority) +if (/^\d+$/.test(category)) { + const currentPage = parseInt(category, 10); + + const itemsPerPage = 30; + // Calculate totals + const { totalMcpCount: totalServers, totalCategoryCount } = await getOverview(); + const totalTools = 0; // We don't track tool count separately in DB yet + const totalClients = 0; + + const totalPages = Math.ceil(totalCategoryCount / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + + // Fetch categories data directly + const allCategories = await getAllMcpCategories(currentPage, itemsPerPage); + + // Map DB categories to the format expected by the component + const mappedCategories = allCategories.map(cat => ({ + id: cat.slug, + name: cat.name, + description: cat.description, + icon: cat.slug, + serverCount: cat.count, + url: `/freedevtools/mcp/${cat.slug}/1/` + })); + + const currentPageCategories = mappedCategories; + + // SEO data + const title = `Awesome MCP Servers Directory – Discover ${allCategories.length} Model Context Protocol Tools & Categories (Page ${currentPage} of ${totalPages}) | Free DevTools by Hexmos`; + const description = `Explore ${totalServers}+ verified MCP servers, tools, and clients used by Claude, Cursor, and Windsurf. Find Model Context Protocol integrations for your AI apps — free, open source, and easy to use.`; + const keywords = [ + 'MCP', + 'Model Context Protocol', + 'MCP servers', + 'MCP tools', + 'MCP clients', + 'AI tools', + 'developer tools', + 'open source', + 'repositories', + 'directory', + 'pagination', + ]; + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, + ]; + paginationData = { + currentPage, + totalPages, + totalCategories: totalCategoryCount, + categories: currentPageCategories, + totalServers, + totalTools, + totalClients, + breadcrumbItems, + title, + description, + keywords, + }; +} else { + // Validate category exists + const categoryData = await getMcpCategory(category); + if (!categoryData) { + return new Response(null, { status: 404 }); + } + + // Redirect to the first page + shouldRedirect = true; + redirectUrl = `/freedevtools/mcp/${category}/1/`; } -// Redirect to the first page -return Astro.redirect(`/freedevtools/mcp/${category}/1/`); +if (shouldRedirect) { + return Astro.redirect(redirectUrl, 301); +} --- + +{paginationData ? ( + + + +) : null} diff --git a/frontend/src/pages/mcp/[category]/sitemap.xml.ts b/frontend/src/pages/mcp/[category]/sitemap.xml.ts index 7ed94f0459..ae71858044 100644 --- a/frontend/src/pages/mcp/[category]/sitemap.xml.ts +++ b/frontend/src/pages/mcp/[category]/sitemap.xml.ts @@ -1,14 +1,9 @@ import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; +import { + getAllMcpPageKeysByCategory, +} from 'db/mcp/mcp-utils'; -// Generate static paths for all MCP categories -export async function getStaticPaths() { - const categoryEntries = await getCollection('mcpCategoryData' as any); - - return categoryEntries.map((entry: any) => ({ - params: { category: entry.data.category }, - })); -} +export const prerender = false; export const GET: APIRoute = async ({ site, params }) => { const now = new Date().toISOString(); @@ -19,21 +14,15 @@ export const GET: APIRoute = async ({ site, params }) => { return new Response('Category not found', { status: 404 }); } - // Load the specific category data - const categoryEntries = await getCollection('mcpCategoryData' as any); - const categoryEntry = categoryEntries.find( - (entry: any) => entry.data.category === category - ); + // Fetch keys from DB + const repoKeys = await getAllMcpPageKeysByCategory(category); - if (!categoryEntry) { - return new Response('Category not found', { status: 404 }); + if (!repoKeys || repoKeys.length === 0) { + return new Response('Category not found or empty', { status: 404 }); } - const categoryData = (categoryEntry as any).data; - const repositories = categoryData.repositories; - // Create URLs for all repositories in this category - const urls = Object.keys(repositories).map((repoId) => { + const urls = repoKeys.map((repoId) => { return ` ${site}/mcp/${category}/${repoId}/ @@ -43,31 +32,6 @@ export const GET: APIRoute = async ({ site, params }) => { `; }); - // Calculate pagination for this category - const totalRepositories = Object.keys(repositories).length; - const itemsPerPage = 30; - const totalPages = Math.ceil(totalRepositories / itemsPerPage); - - // Add the category page itself (redirects to page 1) - urls.unshift(` - - ${site}/mcp/${category}/1/ - ${now} - daily - 0.9 - `); - - // Add pagination pages (2, 3, 4, etc. - skip page 1 as it's the same as category/1/) - for (let i = 2; i <= totalPages; i++) { - urls.push(` - - ${site}/mcp/${category}/${i}/ - ${now} - daily - 0.8 - `); - } - // Split URLs into chunks if needed const sitemapChunks: string[][] = []; for (let i = 0; i < urls.length; i += MAX_URLS) { @@ -82,10 +46,10 @@ export const GET: APIRoute = async ({ site, params }) => { if (!chunk) return new Response('Not Found', { status: 404 }); const xml = ` - - - ${chunk.join('\n')} -`; + + + ${chunk.join('\n')} + `; return new Response(xml, { headers: { @@ -98,17 +62,17 @@ export const GET: APIRoute = async ({ site, params }) => { // If we have multiple chunks, serve sitemap index if (sitemapChunks.length > 1) { const indexXml = ` - - - ${sitemapChunks - .map( - (_, i) => ` + + + ${sitemapChunks + .map( + (_, i) => ` ${site}/mcp/${category}/sitemap-${i + 1}.xml ${now} - ` - ) - .join('\n')} + ` + ) + .join('\n')} `; return new Response(indexXml, { diff --git a/frontend/src/pages/mcp/[page].astro b/frontend/src/pages/mcp/[page].astro deleted file mode 100644 index 6883520fb3..0000000000 --- a/frontend/src/pages/mcp/[page].astro +++ /dev/null @@ -1,85 +0,0 @@ ---- -import BaseLayout from '@/layouts/BaseLayout.astro'; -import { generateMcpDirectoryPaginatedPaths } from '@/lib/mcp-utils'; -import { getCollection } from 'astro:content'; -import Mcp from './_Mcp.astro'; - -// Generate static paths for MCP directory with pagination -export async function getStaticPaths({ paginate }) { - return await generateMcpDirectoryPaginatedPaths({ paginate }); -} - -// Get page from props -const { page } = Astro.props; - -// Load MCP metadata -const metadataEntries = await getCollection('mcpMetadata'); -const metadata = metadataEntries[0]?.data; - -if (!metadata) { - throw new Error('MCP metadata not found'); -} - -// Calculate totals -const totalServers = metadata.totalRepositories; -const totalTools = Object.values(metadata.categories).reduce( - (sum, cat) => sum + cat.npmPackages, - 0 -); -const totalClients = 0; // This would need to be calculated from actual data - -// SEO data -const title = `Awesome MCP Servers Directory – Discover ${page.total} Model Context Protocol Tools & Categories (Page ${page.currentPage} of ${page.lastPage}) | Free DevTools by Hexmos`; -const description = `Explore ${page.total}+ verified MCP servers, tools, and clients used by Claude, Cursor, and Windsurf. Find Model Context Protocol integrations for your AI apps — free, open source, and easy to use.`; -const keywords = [ - 'MCP', - 'Model Context Protocol', - 'MCP servers', - 'MCP tools', - 'MCP clients', - 'AI tools', - 'developer tools', - 'open source', - 'repositories', - 'directory', - 'pagination', -]; - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, -]; ---- - - - - diff --git a/frontend/src/pages/mcp/_McpRepoCard.astro b/frontend/src/pages/mcp/_McpRepoCard.astro index e610f807fc..59a69aa0a6 100644 --- a/frontend/src/pages/mcp/_McpRepoCard.astro +++ b/frontend/src/pages/mcp/_McpRepoCard.astro @@ -1,32 +1,32 @@ --- import { formatNumber } from '@/lib/utils'; -const { server, formattedName, category, repositoryId } = Astro.props; +const { name, description, owner, stars, updated_at,imageUrl, license, category, repositoryId, npm_downloads } = Astro.props; ---
{/* First Row: Title */}

- {formattedName} + {name}

{/* Second Row: Author */}
- {server.owner || 'Unknown Author'} + {owner || 'Unknown Author'}
@@ -37,10 +37,10 @@ const { server, formattedName, category, repositoryId } = Astro.props; class="w-16 h-16 rounded-lg bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 flex items-center justify-center relative overflow-hidden" > { - server.imageUrl ? ( + imageUrl ? ( {`${formattedName} @@ -48,7 +48,7 @@ const { server, formattedName, category, repositoryId } = Astro.props; }
- {server.description} + {description}

@@ -87,7 +87,7 @@ const { server, formattedName, category, repositoryId } = Astro.props; Last Updated { - new Date(server.updated_at).toLocaleDateString('en-US', { + new Date(updated_at).toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', @@ -116,7 +116,7 @@ const { server, formattedName, category, repositoryId } = Astro.props; d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" > - {formatNumber(server.stars)} + {formatNumber(stars)}
- {formatNumber(server.npm_downloads)} + {formatNumber(npm_downloads)}
- {server.license} + {license}
diff --git a/frontend/src/pages/mcp/sitemap.xml.ts b/frontend/src/pages/mcp/sitemap.xml.ts index c5e3d798ab..e851cf231d 100644 --- a/frontend/src/pages/mcp/sitemap.xml.ts +++ b/frontend/src/pages/mcp/sitemap.xml.ts @@ -1,25 +1,11 @@ import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; +import { getAllMcpCategories } from 'db/mcp/mcp-utils'; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); - // Get all categories from the metadata - const metadataEntries = await getCollection('mcpMetadata' as any); - const metadata = (metadataEntries[0] as any)?.data; - - if (!metadata) { - return new Response('Metadata not found', { status: 500 }); - } - - // Get all categories from the metadata - const categories = Object.keys(metadata.categories); - - // Calculate total pages for MCP directory pagination as - - const totalCategories = categories.length; - const itemsPerPage = 30; - const totalPages = Math.ceil(totalCategories / itemsPerPage); + // Get all categories from the database + const categories = await getAllMcpCategories(); // Create sitemap index - MCP pages sitemap + category sitemaps const sitemapIndex = ` @@ -30,14 +16,14 @@ export const GET: APIRoute = async ({ site }) => { ${now} ${categories - .map( - (category) => ` + .map( + (category) => ` - ${site}/mcp/${category}/sitemap.xml + ${site}/mcp/${category.slug}/sitemap.xml ${now} ` - ) - .join('')} + ) + .join('')} `; return new Response(sitemapIndex, { diff --git a/frontend/src/pages/png_icons/[category].astro b/frontend/src/pages/png_icons/[category].astro index ab0993a945..c1b998912e 100644 --- a/frontend/src/pages/png_icons/[category].astro +++ b/frontend/src/pages/png_icons/[category].astro @@ -1,291 +1,433 @@ --- import { - getClusterByName, - getClusters, - getIconsByCluster, +getClusterByName, +getClustersWithPreviewIcons, +getIconsByCluster, +getTotalClusters, +getTotalIcons, } from 'db/png_icons/png-icons-utils'; import AdBanner from '../../components/banner/AdBanner.astro'; import CreditsButton from '../../components/buttons/CreditsButton'; import ToolContainer from '../../components/tool/ToolContainer'; import ToolHead from '../../components/tool/ToolHead'; import BaseLayout from '../../layouts/BaseLayout.astro'; +import PngIcons from './_PngIcons.astro'; -export async function getStaticPaths() { - const clusters = getClusters(); +export const prerender = false; - // Use cluster names for URLs (these match cluster_svg.json cluster.name) - const categories = clusters.map((cluster) => cluster.name); +// Track request start time +const requestStartTime = Date.now(); +const { category } = Astro.params; +const urlPath = Astro.url.pathname; +console.log(`[PNG_ICONS] Request reached server: ${urlPath} at ${new Date().toISOString()}`); - return categories.map((category) => ({ - params: { category }, - props: { category }, - })); +if (!category) { + return new Response(null, { status: 404 }); } -const { category } = Astro.params; +let paginationData: any = null; +let categoryData: any = null; -// Get cluster data from database -const clusterData = getClusterByName(category); +// If category is numeric, this is actually a pagination route +// Handle it directly (workaround for route priority) +if (/^\d+$/.test(category)) { + const currentPage = parseInt(category, 10); -if (!clusterData) { - return Astro.redirect('/freedevtools/png_icons/'); -} + // Redirect /png_icons/1 to /png_icons + if (currentPage === 1) { + return Astro.redirect('/freedevtools/png_icons/'); + } -async function getCategoryIcons() { - // Get icons from SQLite database - // The category parameter is the cluster display name (from cluster.name in cluster_svg.json) - // Look up cluster by display name to get the source_folder (actual folder name used in icon table) - // The icon table uses cluster key (folder name = source_folder), not display name - // Use source_folder to query icons - // clusterData is guaranteed to exist due to check above - const dbIcons = getIconsByCluster(clusterData!.source_folder || category!); + const itemsPerPage = 30; + + // Run all queries in parallel for better performance + const queriesStartTime = Date.now(); + const [totalCategories, categoriesWithIconsResult, totalPngIcons] = await Promise.all([ + getTotalClusters(), + getClustersWithPreviewIcons(currentPage, itemsPerPage, 6, true) as Promise, + getTotalIcons(), + ]); + const queriesEndTime = Date.now(); + console.log(`[PNG_ICONS] All queries (parallel) took ${queriesEndTime - queriesStartTime}ms`); + + const totalPages = Math.ceil(totalCategories / itemsPerPage); - const icons = dbIcons.map((icon) => { - const iconName = icon.name.replace(/\.(png|svg)$/i, ''); + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + // Transform to match expected format + const currentPageCategories = categoriesWithIconsResult.map((cluster) => { return { - name: iconName, - description: icon.description || `Free ${iconName} icon`, - category: category, - tags: icon.tags || [], - author: 'Free DevTools', - license: 'MIT', - url: `/freedevtools/png_icons/${category}/${iconName}/`, - base64: icon.base64, - img_alt: icon.img_alt, + id: cluster.source_folder || cluster.name, + name: cluster.name, + icon: `/freedevtools/png_icons/${cluster.name}/`, + iconCount: cluster.iconCount || cluster.count, + url: `/freedevtools/png_icons/${cluster.name}/`, + previewIcons: cluster.previewIcons || [], }; }); - return icons; -} + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'PNG Icons', href: '/freedevtools/png_icons/' }, + { label: `Page ${currentPage}` }, + ]; + + // SEO data + const seoTitle = `Free PNG Icons - Page ${currentPage} | Online Free DevTools by Hexmos | No Registration Required`; + const seoDescription = `Page ${currentPage} of ${totalPages} in our PNG icon archive. Get free vector icons with fast, no-login downloads.`; + const canonical = `https://hexmos.com/freedevtools/png_icons/${currentPage}/`; + + const paginatedKeywords = [ + 'png icons', + 'vector graphics', + 'free icons', + 'download icons', + 'edit icons', + `page ${currentPage}`, + 'pagination', + 'icon collection', + 'vector graphics library', + ]; + + // Log total roundtrip time for pagination + const requestEndTime = Date.now(); + const totalRoundtripTime = requestEndTime - requestStartTime; + console.log(`[PNG_ICONS] Total roundtrip time for ${urlPath}: ${totalRoundtripTime}ms`); + + paginationData = { + currentPage, + totalPages, + totalCategories, + totalPngIcons, + categories: currentPageCategories, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + paginatedKeywords, + itemsPerPage, + }; +} else { + // Redirect to add trailing slash if missing + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + // Get cluster data from database + const clusterQueryStartTime = Date.now(); + const clusterData = await getClusterByName(category); + const clusterQueryEndTime = Date.now(); + console.log(`[PNG_ICONS] getClusterByName("${category}") took ${clusterQueryEndTime - clusterQueryStartTime}ms`); + + if (!clusterData) { + return new Response(null, { status: 404 }); + } + + async function getCategoryIcons() { + const iconQueryStartTime = Date.now(); + const dbIcons = await getIconsByCluster(clusterData!.source_folder || category!); + const iconQueryEndTime = Date.now(); + console.log(`[PNG_ICONS] getIconsByCluster("${clusterData!.source_folder || category!}") took ${iconQueryEndTime - iconQueryStartTime}ms`); + + const icons = dbIcons.map((icon) => { + const iconName = icon.name.replace(/\.(png|svg)$/i, ''); + + return { + name: iconName, + description: icon.description || `Free ${iconName} icon`, + category: category, + tags: icon.tags || [], + author: 'Free DevTools', + license: 'MIT', + url: `/freedevtools/png_icons/${category}/${iconName}/`, + base64: icon.base64, + img_alt: icon.img_alt, + }; + }); + + return icons; + } -const categoryIcons = await getCategoryIcons(); -const totalIcons = categoryIcons.length; - -// Use database content for about and why_choose_us -// Fallback to default content if not available in database -const aboutContent = - clusterData.about || - `Our ${category} PNG icon collection provides high-quality raster graphics optimized for digital use. Each icon features a transparent background and is optimized for web performance. Whether you're building websites, apps, or presentations, these ${category} PNG icons are ready to use right away.`; - -const whyChooseUsContent = - clusterData.why_choose_us && clusterData.why_choose_us.length > 0 - ? clusterData.why_choose_us - : [ - 'Transparent backgrounds for easy integration', - 'High-quality raster format perfect for web use', - 'No conversion needed - ready to use immediately', - 'Compatible with all design software and browsers', - ]; - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'PNG Icons', href: '/freedevtools/png_icons/' }, - { label: category }, -]; + const categoryIcons = await getCategoryIcons(); + const totalIcons = categoryIcons.length; + + // Log total roundtrip time + const requestEndTime = Date.now(); + const totalRoundtripTime = requestEndTime - requestStartTime; + console.log(`[PNG_ICONS] Total roundtrip time for ${urlPath}: ${totalRoundtripTime}ms`); + + // Use database content for about and why_choose_us + const aboutContent = + clusterData.about || + `Our ${category} PNG icon collection provides high-quality raster graphics optimized for digital use. Each icon features a transparent background and is optimized for web performance. Whether you're building websites, apps, or presentations, these ${category} PNG icons are ready to use right away.`; + + const whyChooseUsContent = + clusterData.why_choose_us && clusterData.why_choose_us.length > 0 + ? clusterData.why_choose_us + : [ + 'Transparent backgrounds for easy integration', + 'High-quality raster format perfect for web use', + 'No conversion needed - ready to use immediately', + 'Compatible with all design software and browsers', + ]; + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'PNG Icons', href: '/freedevtools/png_icons/' }, + { label: category }, + ]; + + categoryData = { + category, + clusterData, + categoryIcons, + totalIcons, + aboutContent, + whyChooseUsContent, + breadcrumbItems, + }; +} --- - 0 - ? clusterData.keywords - : [ - category, - 'png icons', - 'raster images', - 'transparent png', - 'web graphics', - 'free icons', - ]} -> - -
- -
- + - -
-
- { - categoryIcons.map((icon) => { - const iconName = icon.name - .replace(/_/g, ' ') - .replace(/\.(png|svg)$/i, '') - .replace(/\b\w/g, (l: string) => l.toUpperCase()); - - return ( - -
-
-
- { -
-
-
-
- ); - }) - } + +) : categoryData ? ( + 0 + ? categoryData.clusterData.keywords + : [ + categoryData.category, + 'png icons', + 'raster images', + 'transparent png', + 'web graphics', + 'free icons', + ]} + > + +
+
-
- - { - clusterData.practical_application && ( -
-

- Practical Application -

-

{clusterData.practical_application}

-
- ) - } + - - { - aboutContent && ( +
-

- About {category.charAt(0).toUpperCase() + category.slice(1)} PNG - Icons -

-

{aboutContent}

-
- ) - } + { + categoryData.categoryIcons.map((icon) => { + const iconName = icon.name + .replace(/_/g, ' ') + .replace(/\.(png|svg)$/i, '') + .replace(/\b\w/g, (l: string) => l.toUpperCase()); - - { - whyChooseUsContent && whyChooseUsContent.length > 0 && ( -
-

- Why Choose our PNG Icons? -

-
    - {whyChooseUsContent.map((item) => ( -
  • {item}
  • - ))} -
+ return ( + +
+
+
+ { +
+
+
+
+ ); + }) + }
- ) - } - - - { - clusterData.alternative_terms && - clusterData.alternative_terms.length > 0 && ( +
+ + { + categoryData.clusterData.practical_application && (

- Alternative Terms + Practical Application

-
- {clusterData.alternative_terms.map((term) => ( - - {term} - +

{categoryData.clusterData.practical_application}

+
+ ) + } + + + { + categoryData.aboutContent && ( +
+

+ About {categoryData.category.charAt(0).toUpperCase() + categoryData.category.slice(1)} PNG + Icons +

+

{categoryData.aboutContent}

+
+ ) + } + + + { + categoryData.whyChooseUsContent && categoryData.whyChooseUsContent.length > 0 && ( +
+

+ Why Choose our PNG Icons? +

+
    + {categoryData.whyChooseUsContent.map((item) => ( +
  • {item}
  • ))} -
+
) - } + } - - { - clusterData.tags && clusterData.tags.length > 0 && ( -
-

- Tags -

-
- {clusterData.tags.map((tag) => ( -

- {tag} -

- ))} + + { + categoryData.clusterData.alternative_terms && + categoryData.clusterData.alternative_terms.length > 0 && ( +
+

+ Alternative Terms +

+
+ {categoryData.clusterData.alternative_terms.map((term) => ( + + {term} + + ))} +
+
+ ) + } + + + { + categoryData.clusterData.tags && categoryData.clusterData.tags.length > 0 && ( +
+

+ Tags +

+
+ {categoryData.clusterData.tags.map((tag) => ( +

+ {tag} +

+ ))} +
-
- ) - } - - -
- - + + +) : null} + +{(() => { + if (!dataPhaseDuration) { + return null; + } + const renderEndTime = Date.now(); + const totalRenderTime = renderEndTime - requestStartTime; + const renderPhaseDuration = Math.max(0, totalRenderTime - dataPhaseDuration); + console.log( + `[SVG_ICONS] Full SSR render for ${urlPath} took ${totalRenderTime}ms (data prep ${dataPhaseDuration}ms, layout/render ${renderPhaseDuration}ms)` + ); + return null; +})()} diff --git a/frontend/src/pages/svg_icons/[category]/[icon].astro b/frontend/src/pages/svg_icons/[category]/[icon].astro index ab5c325a0b..1ed55bea72 100644 --- a/frontend/src/pages/svg_icons/[category]/[icon].astro +++ b/frontend/src/pages/svg_icons/[category]/[icon].astro @@ -1,42 +1,18 @@ --- -import { - getClusters, - getIconByCategoryAndName, -} from 'db/svg_icons/svg-icons-utils'; +import { getIconByCategoryAndName } from 'db/svg_icons/svg-icons-utils'; import AdBanner from '../../../components/banner/AdBanner.astro'; import Banner from '../../../components/banner/BannerIndex.astro'; import CopyPngButton from '../../../components/buttons/CopyPngButton.tsx'; import CopySvgButton from '../../../components/buttons/CopySvgButton.tsx'; -import CreditsButton from '../../../components/buttons/CreditsButton'; +import CreditsButton from '../../../components/buttons/CreditsButton.tsx'; import DownloadPngButton from '../../../components/buttons/DownloadPngButton.tsx'; import DownloadSvgButton from '../../../components/buttons/DownloadSvgButton.tsx'; import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; -import ToolContainer from '../../../components/tool/ToolContainer'; -import ToolHead from '../../../components/tool/ToolHead'; +import ToolContainer from '../../../components/tool/ToolContainer.tsx'; +import ToolHead from '../../../components/tool/ToolHead.tsx'; import BaseLayout from '../../../layouts/BaseLayout.astro'; -export async function getStaticPaths() { - const clusters = getClusters(); - const paths: Array<{ params: { category: string; icon: string } }> = []; - - // Process each cluster (category) - for (const cluster of clusters) { - // Get all icons for this cluster using source_folder (cluster key) - const { getIconsByCluster } = await import('db/svg_icons/svg-icons-utils'); - const icons = getIconsByCluster(cluster.source_folder || cluster.name); - - // Process each icon - for (const iconRecord of icons) { - const iconName = iconRecord.name.replace('.svg', ''); - - paths.push({ - params: { category: cluster.name, icon: iconName }, - } as { params: { category: string; icon: string } }); - } - } - - return paths; -} +export const prerender = false; const { category, icon } = Astro.params; @@ -130,17 +106,16 @@ function getSvgIconSeoContent(iconName: string, categoryName: string) { } // Get icon data from SQLite database -const iconRecord = getIconByCategoryAndName(category || '', icon || ''); +const iconRecord = await getIconByCategoryAndName(category || '', icon || ''); if (!iconRecord) { return Astro.redirect('/freedevtools/svg_icons/'); } // Prepare icon data for the component -const iconName = iconRecord.name.replace('.svg', ''); const iconData = { - name: iconName, - description: iconRecord.description || `Free ${iconName} icon`, + name: iconRecord.name, + description: iconRecord.description || `Free ${iconRecord.name} icon`, category: category || '', tags: iconRecord.tags || [], author: 'Free DevTools by Hexmos', @@ -275,7 +250,6 @@ const encodingFormat = class="grid grid-cols-2 gap-2 w-full max-w-md" > - diff --git a/frontend/src/pages/svg_icons/[page].astro b/frontend/src/pages/svg_icons/[page].astro deleted file mode 100644 index 75ba46f48c..0000000000 --- a/frontend/src/pages/svg_icons/[page].astro +++ /dev/null @@ -1,125 +0,0 @@ ---- -import { getClusters, getIconsByCluster } from 'db/svg_icons/svg-icons-utils'; -import BaseLayout from '../../layouts/BaseLayout.astro'; -import SvgIcons from './_SvgIcons.astro'; - -// Generate static paths for paginated routes -export async function getStaticPaths() { - const clusters = getClusters(); - - // Get all categories with their data - const categories = clusters.map((cluster) => { - const icons = getIconsByCluster(cluster.source_folder || cluster.name); - - return { - id: cluster.source_folder || cluster.name, - name: cluster.name, - description: cluster.description, - icon: `/freedevtools/svg_icons/${cluster.name}/`, - iconCount: icons.length, - url: `/freedevtools/svg_icons/${cluster.name}/`, - keywords: cluster.keywords, - features: cluster.tags, - fileNames: icons.map((icon) => icon.name), - }; - }); - - const itemsPerPage = 30; - const totalPages = Math.ceil(categories.length / itemsPerPage); - const paths: any[] = []; - - // Generate pagination pages (2, 3, 4, etc. - page 1 is handled by index.astro) - for (let i = 2; i <= totalPages; i++) { - paths.push({ - params: { page: i.toString() }, - props: { - type: 'pagination', - page: i, - itemsPerPage, - totalPages, - categories, - }, - }); - } - - return paths; -} - -const { type, page, itemsPerPage, totalPages, categories } = Astro.props; - -// Redirect /svg_icons/1 to /svg_icons -if (type === 'pagination' && page === 1) { - return Astro.redirect('/freedevtools/svg_icons/'); -} - -const currentPage = page; -const totalCategories = categories.length; - -// Calculate total SVG icons across all categories -const totalSvgIcons = categories.reduce( - (total: number, category: any) => total + category.iconCount, - 0 -); - -// Get categories for current page -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const currentPageCategories = categories.slice(startIndex, endIndex); - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'SVG Icons', href: '/freedevtools/svg_icons/' }, - { label: `Page ${currentPage}` }, -]; - -// SEO data -const seoTitle = `Free SVG Icons - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our free SVG icons collection. Download thousands of vector graphics instantly. No registration required.`; -const canonical = `https://hexmos.com/freedevtools/svg_icons/${currentPage}/`; - -// Enhanced keywords for paginated content -const paginatedKeywords = [ - 'svg icons', - 'vector graphics', - 'free icons', - 'download icons', - 'edit icons', - `page ${currentPage}`, - 'pagination', - 'icon collection', - 'vector graphics library', -]; ---- - - - - diff --git a/frontend/src/pages/svg_icons/_SvgIcons.astro b/frontend/src/pages/svg_icons/_SvgIcons.astro index 764d580b20..e0acd75bfe 100644 --- a/frontend/src/pages/svg_icons/_SvgIcons.astro +++ b/frontend/src/pages/svg_icons/_SvgIcons.astro @@ -11,7 +11,7 @@ import { MixerHorizontalIcon, StarIcon, } from '@radix-ui/react-icons'; -import { getIconsByCluster } from 'db/svg_icons/svg-icons-utils'; +// Preview icons are now included in the category data from the optimized query import AdBanner from '../../components/banner/AdBanner.astro'; const { @@ -70,21 +70,18 @@ const { > { categories.map((category: any) => { - // Get base64 icons from database for this category - // Use category.id (source_folder) which is the actual cluster key in the database - const dbIcons = getIconsByCluster(category.id || category.name) || []; - const previewIcons = dbIcons.slice(0, 6); + // Use preview icons already included in category data (from optimized single query) + const previewIcons = category.previewIcons || []; const iconPreviews = previewIcons .map((icon: any) => { - const iconName = icon.name.replace('.svg', ''); return `
${iconName} icon @@ -134,6 +131,7 @@ const { currentPage={currentPage} totalPages={totalPages} baseUrl="/freedevtools/svg_icons/" + alwaysIncludePageNumber={true} /> diff --git a/frontend/src/pages/svg_icons/index.astro b/frontend/src/pages/svg_icons/index.astro index 8bc6556e18..6ff8250145 100644 --- a/frontend/src/pages/svg_icons/index.astro +++ b/frontend/src/pages/svg_icons/index.astro @@ -1,44 +1,47 @@ --- import { - getClusters, - getIconsByCluster, + getClustersWithPreviewIcons, + getTotalClusters, getTotalIcons, + type ClusterTransformed, } from 'db/svg_icons/svg-icons-utils'; import BaseLayout from '../../layouts/BaseLayout.astro'; import SvgIcons from './_SvgIcons.astro'; -// Load SVG icons data from SQLite -const clusters = getClusters(); - -// Get all categories with their data -const allCategories = clusters.map((cluster) => { - // Get icons for this cluster to count them - const icons = getIconsByCluster(cluster.source_folder || cluster.name); - - return { - id: cluster.source_folder || cluster.name, - name: cluster.name, - description: cluster.description, - icon: `/freedevtools/svg_icons/${cluster.name}/`, - iconCount: icons.length, - url: `/freedevtools/svg_icons/${cluster.name}/`, - keywords: cluster.keywords, - features: cluster.tags, - fileNames: icons.map((icon) => icon.name), // Include fileNames for icon previews - }; -}); - -// Calculate total SVG icons across all categories -const totalSvgIcons = getTotalIcons(); +// Track request start time +const requestStartTime = Date.now(); +const requestPath = Astro.url.pathname; // Pagination logic for page 1 const itemsPerPage = 30; const currentPage = 1; -const totalPages = Math.ceil(allCategories.length / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const categories = allCategories.slice(startIndex, endIndex); -const totalCategories = allCategories.length; + +// Run all queries in parallel for better performance +const queriesStartTime = Date.now(); +const [totalCategories, categoriesResult, totalSvgIcons] = await Promise.all([ + getTotalClusters(), + getClustersWithPreviewIcons( + currentPage, + itemsPerPage, + 6, + true // transform = true to get transformed format + ) as Promise, + getTotalIcons(), +]); +const queriesEndTime = Date.now(); +console.log( + `[SVG_ICONS] All queries (parallel) took ${queriesEndTime - queriesStartTime}ms` +); + +const categories = categoriesResult; +const totalPages = Math.ceil(totalCategories / itemsPerPage); + +// Log total roundtrip time +const requestEndTime = Date.now(); +const totalRoundtripTime = requestEndTime - requestStartTime; +console.log( + `[SVG_ICONS] Server scripts (before template render) for ${requestPath} took ${totalRoundtripTime}ms` +); // Breadcrumb data const breadcrumbItems = [ diff --git a/frontend/src/pages/svg_icons/sitemap-[index].xml.ts b/frontend/src/pages/svg_icons/sitemap-[index].xml.ts index e51da037bd..076cc632b2 100644 --- a/frontend/src/pages/svg_icons/sitemap-[index].xml.ts +++ b/frontend/src/pages/svg_icons/sitemap-[index].xml.ts @@ -1,65 +1,58 @@ // src/pages/svg_icons/sitemap-[index].xml.ts import type { APIRoute } from 'astro'; -import path from 'path'; const MAX_URLS = 5000; -export async function getStaticPaths() { - const { glob } = await import('glob'); +// Loader function for sitemap URLs - extracted to work in both SSG and SSR +async function loadUrls() { + // Get all icons from database instead of globbing files + const { query } = await import('db/svg_icons/svg-worker-pool'); + const icons = await query.getSitemapIcons(); + const now = new Date().toISOString(); - // Loader function for sitemap URLs - async function loadUrls() { - const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); - const now = new Date().toISOString(); + // Build URLs with placeholder for site + const urls = icons.map((icon) => { + const category = icon.category || icon.cluster; + const name = icon.name; - // Build URLs with placeholder for site - const urls = svgFiles.map((file) => { - const parts = file.split(path.sep); - const name = parts.pop()!.replace('.svg', ''); - const category = parts.pop() || 'general'; - - return ` - - __SITE__/svg_icons/${category}/${name}/ - ${now} - daily - 0.8 - - __SITE__/svg_icons/${category}/${name}.svg - Free ${name} SVG Icon Download - - `; - }); - - // Include landing page - urls.unshift(` + return ` - __SITE__/svg_icons/ + __SITE__/svg_icons/${category}/${name}/ ${now} daily - 0.9 - `); - - return urls; - } + 0.8 + + __SITE__/svg_icons/${category}/${name}.svg + Free ${name} SVG Icon Download + + `; + }); - // Pre-count total pages - const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); - const totalUrls = svgFiles.length + 1; - const totalPages = Math.ceil(totalUrls / MAX_URLS); + // Include landing page + urls.unshift(` + + __SITE__/svg_icons/ + ${now} + daily + 0.9 + `); - return Array.from({ length: totalPages }, (_, i) => ({ - params: { index: String(i + 1) }, - props: { loadUrls }, // pass only the function reference - })); + return urls; } -export const GET: APIRoute = async ({ site, params, props }) => { - const loadUrls: () => Promise = props.loadUrls; +export const prerender = false; + +export const GET: APIRoute = async ({ site, params }) => { + // SSR mode: call loadUrls directly let urls = await loadUrls(); + // Use site from .env file (SITE variable) or astro.config.mjs + const envSite = process.env.SITE; + const siteStr = site?.toString() || ''; + const siteUrl = envSite || siteStr || 'http://localhost:4321/freedevtools'; + // Replace placeholder with actual site - urls = urls.map((u) => u.replace(/__SITE__/g, site)); + urls = urls.map((u) => u.replace(/__SITE__/g, siteUrl)); // Split into chunks const sitemapChunks: string[][] = []; @@ -67,7 +60,7 @@ export const GET: APIRoute = async ({ site, params, props }) => { sitemapChunks.push(urls.slice(i, i + MAX_URLS)); } - const index = parseInt(params.index, 10) - 1; + const index = parseInt(params?.index || '1', 10) - 1; const chunk = sitemapChunks[index]; if (!chunk) return new Response('Not Found', { status: 404 }); diff --git a/frontend/src/pages/svg_icons/sitemap.xml.ts b/frontend/src/pages/svg_icons/sitemap.xml.ts index ff3584413e..6c451157c7 100644 --- a/frontend/src/pages/svg_icons/sitemap.xml.ts +++ b/frontend/src/pages/svg_icons/sitemap.xml.ts @@ -1,29 +1,36 @@ import type { APIRoute } from 'astro'; -export const GET: APIRoute = async ({ site, params }) => { - const { glob } = await import('glob'); - const path = await import('path'); +export const prerender = false; +export const GET: APIRoute = async ({ site, params }) => { const now = new Date().toISOString(); const MAX_URLS = 5000; - // Get all SVG files - const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); + // Always use site from .env file (SITE variable) or astro.config.mjs + // NODE_ENV can be "dev" or "prod" (lowercase) + const envSite = process.env.SITE; + const siteStr = site?.toString() || ''; + + // Use SITE from .env if available, otherwise use site parameter, otherwise fallback + const siteUrl = envSite || siteStr || 'http://localhost:4321/freedevtools'; + + // Get all icons from database instead of globbing files + const { query } = await import('db/svg_icons/svg-worker-pool'); + const icons = await query.getSitemapIcons(); - // Map files to sitemap URLs with image info - const urls = svgFiles.map((file) => { - const parts = file.split(path.sep); - const name = parts.pop()!.replace('.svg', ''); - const category = parts.pop() || 'general'; + // Map icons to sitemap URLs with image info + const urls = icons.map((icon) => { + const category = icon.category || icon.cluster; + const name = icon.name; return ` - ${site}/svg_icons/${category}/${name}/ + ${siteUrl}/svg_icons/${category}/${name}/ ${now} daily 0.8 - ${site}/svg_icons/${category}/${name}.svg + ${siteUrl}/svg_icons/${category}/${name}.svg Free ${name} SVG Icon Download `; @@ -32,7 +39,7 @@ export const GET: APIRoute = async ({ site, params }) => { // Include the landing page urls.unshift(` - ${site}/svg_icons/ + ${siteUrl}/svg_icons/ ${now} daily 0.9 @@ -71,14 +78,14 @@ export const GET: APIRoute = async ({ site, params }) => { - ${site}/svg_icons_pages/sitemap.xml + ${siteUrl}/svg_icons_pages/sitemap.xml ${now} ${sitemapChunks .map( (_, i) => ` - ${site}/svg_icons/sitemap-${i + 1}.xml + ${siteUrl}/svg_icons/sitemap-${i + 1}.xml ${now} ` ) diff --git a/frontend/src/pages/svg_icons_pages/sitemap.xml.ts b/frontend/src/pages/svg_icons_pages/sitemap.xml.ts index ab0b6cd476..21785265bd 100644 --- a/frontend/src/pages/svg_icons_pages/sitemap.xml.ts +++ b/frontend/src/pages/svg_icons_pages/sitemap.xml.ts @@ -4,8 +4,13 @@ import { getClusters } from 'db/svg_icons/svg-icons-utils'; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); + // Use site from .env file (SITE variable) or astro.config.mjs + const envSite = process.env.SITE; + const siteStr = site?.toString() || ''; + const siteUrl = envSite || siteStr || 'http://localhost:4321/freedevtools'; + // Get clusters from SQLite database - const clusters = getClusters(); + const clusters = await getClusters(); // Calculate total pages (30 categories per page) const itemsPerPage = 30; @@ -16,7 +21,7 @@ export const GET: APIRoute = async ({ site }) => { // Root SVG icons page urls.push( ` - ${site}/svg_icons/ + ${siteUrl}/svg_icons/ ${now} daily 0.9 @@ -27,7 +32,7 @@ export const GET: APIRoute = async ({ site }) => { for (let i = 2; i <= totalPages; i++) { urls.push( ` - ${site}/svg_icons/${i}/ + ${siteUrl}/svg_icons/${i}/ ${now} daily 0.8 diff --git a/frontend/src/pages/t/base64-[tool]/index.astro b/frontend/src/pages/t/base64-[tool]/index.astro index 120dad2b81..68da943407 100644 --- a/frontend/src/pages/t/base64-[tool]/index.astro +++ b/frontend/src/pages/t/base64-[tool]/index.astro @@ -4,14 +4,7 @@ import { getToolByKey } from '@/config/tools'; import ToolDetailLayout from '@/components/tool/ToolDetailLayout.astro'; import Base64Encoder from './_Base64Encoder'; -// Dynamic routes: tell Astro all possible `tool` keys -export function getStaticPaths() { - return [ - { params: { tool: "encoder" }}, - { params: { tool: "decoder" }}, - { params: { tool: "utilities" }}, - ]; -} +export const prerender = false; // Use the param from route const { tool } = Astro.params; diff --git a/frontend/src/pages/t/html-to-markdown/_HtmlToMarkdown.tsx b/frontend/src/pages/t/html-to-markdown/_HtmlToMarkdown.tsx index ab64a5c89b..22a70b94fa 100644 --- a/frontend/src/pages/t/html-to-markdown/_HtmlToMarkdown.tsx +++ b/frontend/src/pages/t/html-to-markdown/_HtmlToMarkdown.tsx @@ -12,8 +12,8 @@ import { Card, CardContent, CardHeader, CardTitle } from 'src/components/ui/card import CopyButton from 'src/components/ui/copy-button'; import { Label } from 'src/components/ui/label'; import { Textarea } from 'src/components/ui/textarea'; -import HtmlToMarkdownSkeleton from 'src/pages/t/html-to-markdown/_HtmlToMarkdownSkeleton'; import AdBanner from '../../../components/banner/AdBanner'; +import HtmlToMarkdownSkeleton from './_HtmlToMarkdownSkeleton'; const converter = new showdown.Converter(); diff --git a/frontend/src/pages/t/json-[tool]/index.astro b/frontend/src/pages/t/json-[tool]/index.astro index cfb10cc2ca..0b51f1e694 100644 --- a/frontend/src/pages/t/json-[tool]/index.astro +++ b/frontend/src/pages/t/json-[tool]/index.astro @@ -4,17 +4,7 @@ import { getToolByKey } from '@/config/tools'; import ToolDetailLayout from '@/components/tool/ToolDetailLayout.astro'; import JsonPrettifier from './_JsonPrettifier'; -// ✅ Define JSON_KEYS inside the frontmatter block - -// Dynamic routes: tell Astro all possible `tool` keys -export function getStaticPaths() { - return [ - { params: { tool: "prettifier" }}, - { params: { tool: "validator" }}, - { params: { tool: "fixer" }}, - { params: { tool: "utilities" }}, - ]; -} +export const prerender = false; // Use the param from route const { tool } = Astro.params; diff --git a/frontend/src/pages/t/markdown-to-html-converter/_MarkdownToHtmlConverter.tsx b/frontend/src/pages/t/markdown-to-html-converter/_MarkdownToHtmlConverter.tsx index 39f8a5237c..1fc0808adf 100644 --- a/frontend/src/pages/t/markdown-to-html-converter/_MarkdownToHtmlConverter.tsx +++ b/frontend/src/pages/t/markdown-to-html-converter/_MarkdownToHtmlConverter.tsx @@ -12,8 +12,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "src/components/ui/card import CopyButton from "src/components/ui/copy-button"; import { Label } from "src/components/ui/label"; import { Textarea } from "src/components/ui/textarea"; -import MarkdownToHtmlConverterSkeleton from "src/pages/t/markdown-to-html-converter/_MarkdownToHtmlConverterSkeleton"; import AdBanner from "../../../components/banner/AdBanner"; +import MarkdownToHtmlConverterSkeleton from "./_MarkdownToHtmlConverterSkeleton"; const converter = new showdown.Converter(); diff --git a/frontend/src/pages/t/rsa-key-pair-generator/_RsaKeyPairGenerator.tsx b/frontend/src/pages/t/rsa-key-pair-generator/_RsaKeyPairGenerator.tsx index c33f3aeca7..9c9dd4ea7e 100644 --- a/frontend/src/pages/t/rsa-key-pair-generator/_RsaKeyPairGenerator.tsx +++ b/frontend/src/pages/t/rsa-key-pair-generator/_RsaKeyPairGenerator.tsx @@ -18,7 +18,7 @@ import { SelectValue, } from "src/components/ui/select"; import { Textarea } from "src/components/ui/textarea"; -import RsaKeyPairGeneratorSkeleton from "src/pages/t/rsa-key-pair-generator/_RsaKeyPairGeneratorSkeleton"; +import RsaKeyPairGeneratorSkeleton from "./_RsaKeyPairGeneratorSkeleton"; import AdBanner from "../../../components/banner/AdBanner"; const KEY_SIZES = [2048, 4096]; diff --git a/frontend/src/pages/t/zstd-[action]/index.astro b/frontend/src/pages/t/zstd-[action]/index.astro index 5a05d47825..e52cd60ac0 100644 --- a/frontend/src/pages/t/zstd-[action]/index.astro +++ b/frontend/src/pages/t/zstd-[action]/index.astro @@ -4,13 +4,7 @@ import { getToolByKey } from '@/config/tools'; import ToolDetailLayout from '@/components/tool/ToolDetailLayout.astro'; import ZstdCompress from './_ZstdCompress'; -// Tell Astro which pages to generate -export function getStaticPaths() { - return [ - { params: { action: 'compress' } }, - { params: { action: 'decompress' } }, - ]; -} +export const prerender = false; // Get the 'action' from the URL (e.g., 'compress' or 'decompress') const { action } = Astro.params; diff --git a/frontend/src/pages/tldr/[page].astro b/frontend/src/pages/tldr/[page].astro deleted file mode 100644 index e2c2415192..0000000000 --- a/frontend/src/pages/tldr/[page].astro +++ /dev/null @@ -1,81 +0,0 @@ ---- -import BaseLayout from '../../layouts/BaseLayout.astro'; -import { generateTldrStaticPaths } from '../../lib/tldr-utils'; -import Tldr from './_Tldr.astro'; - -// Generate static paths for paginated routes -export async function getStaticPaths() { - return await generateTldrStaticPaths(); -} - -const { type, page, itemsPerPage, totalPages, platforms } = Astro.props; - -// Redirect /tldr/1 to /tldr -if (type === 'pagination' && page === 1) { - return Astro.redirect('/freedevtools/tldr/'); -} - -const currentPage = page; -const totalPlatforms = platforms.length; - -// Calculate total commands across all platforms -const totalCommands = platforms.reduce((total: number, platform: any) => total + platform.count, 0); - -// Get platforms for current page -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const currentPagePlatforms = platforms.slice(startIndex, endIndex); - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'TLDR', href: '/freedevtools/tldr/' }, - { label: `Page ${currentPage}` } -]; - -// SEO data -const seoTitle = `TLDR - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our TLDR command documentation. Learn command-line tools across different platforms.`; -const canonical = `https://hexmos.com/freedevtools/tldr/${currentPage}/`; - -// Enhanced keywords for paginated content -const paginatedKeywords = [ - "tldr", - "command line", - "cli documentation", - "terminal commands", - `page ${currentPage}`, - "pagination", - "command reference", - "cli documentation" -]; - ---- - - - - diff --git a/frontend/src/pages/tldr/[platform]/[command].astro b/frontend/src/pages/tldr/[platform]/[command].astro deleted file mode 100644 index 6b202403c7..0000000000 --- a/frontend/src/pages/tldr/[platform]/[command].astro +++ /dev/null @@ -1,165 +0,0 @@ ---- -import { getCollection, render, type CollectionEntry } from 'astro:content'; -import AdBanner from '../../../components/banner/AdBanner.astro'; -import Banner from '../../../components/banner/BannerIndex.astro'; -import CreditsButton from '../../../components/buttons/CreditsButton'; -import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; -import ToolContainer from '../../../components/tool/ToolContainer'; -import ToolHead from '../../../components/tool/ToolHead'; -import BaseLayout from '../../../layouts/BaseLayout.astro'; - -export async function getStaticPaths() { - const tldrEntries = await getCollection('tldr'); - const paths: Array<{ params: { platform: string; command: string } }> = []; - - for (const entry of tldrEntries) { - // Extract platform and command from the id (which is the file path) - const pathParts = entry.id.split('/'); - const platform = pathParts[pathParts.length - 2]; - const fileName = pathParts[pathParts.length - 1]; - const command = fileName.replace('.md', ''); - - paths.push({ - params: { platform, command }, - }); - } - - return paths; -} - -const { platform, command } = Astro.params; - -// Find the entry by platform and command -const tldrEntries: CollectionEntry<'tldr'>[] = await getCollection('tldr'); -const entry = tldrEntries.find((entry) => { - const pathParts = entry.id.split('/'); - const entryPlatform = pathParts[pathParts.length - 2]; - const fileName = pathParts[pathParts.length - 1]; - const entryCommand = fileName.replace('.md', ''); - return entryPlatform === platform && entryCommand === command; -}); - -if (!entry) { - return Astro.redirect('/404'); -} - -const { Content } = await render(entry); -console.log(Content); -const title = entry.data.title || command; -const description = - entry.data.description || `Documentation for ${command} command`; - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'TLDR', href: '/freedevtools/tldr/' }, - { label: platform, href: `/freedevtools/tldr/${platform}/` }, - { label: command }, -]; ---- - - - - -
- -
- - -
-
- -
-
- { - Array.isArray(entry.data.relatedTools) && - entry.data.relatedTools.length > 0 && ( -
-

- Related Free Online Tools -

- -
- ) - } - - - -
-
diff --git a/frontend/src/pages/tldr/[platform]/[page].astro b/frontend/src/pages/tldr/[platform]/[page].astro deleted file mode 100644 index c6706ce647..0000000000 --- a/frontend/src/pages/tldr/[platform]/[page].astro +++ /dev/null @@ -1,82 +0,0 @@ ---- -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { generateTldrPlatformStaticPaths } from '../../../lib/tldr-utils'; -import TldrPlatform from '../_TldrPlatform.astro'; - -// Generate static paths for paginated platform routes -export async function getStaticPaths() { - return await generateTldrPlatformStaticPaths(); -} - -const { type, page, itemsPerPage, totalPages, commands } = Astro.props; -const { platform } = Astro.params; - -// Redirect /tldr/platform/1 to /tldr/platform -if (type === 'pagination' && page === 1) { - return Astro.redirect(`/freedevtools/tldr/${platform}/`); -} - -const currentPage = page; -const totalCommands = commands.length; - -// Get commands for current page -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const currentPageCommands = commands.slice(startIndex, endIndex); - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'TLDR', href: '/freedevtools/tldr/' }, - { label: platform, href: `/freedevtools/tldr/${platform}/` }, - { label: `Page ${currentPage}` } -]; - -// SEO data -const seoTitle = `${platform} Commands - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our ${platform} command documentation. Learn ${platform} commands quickly with practical examples.`; -const canonical = `https://hexmos.com/freedevtools/tldr/${platform}/${currentPage}/`; - -// Enhanced keywords for paginated platform content -const paginatedPlatformKeywords = [ - `${platform} commands`, - `${platform} cli`, - `${platform} documentation`, - "command line", - "cli documentation", - "terminal commands", - `page ${currentPage}`, - "pagination", - "command reference", - "cli documentation" -]; - ---- - - - - diff --git a/frontend/src/pages/tldr/[platform]/[slug].astro b/frontend/src/pages/tldr/[platform]/[slug].astro new file mode 100644 index 0000000000..c846719187 --- /dev/null +++ b/frontend/src/pages/tldr/[platform]/[slug].astro @@ -0,0 +1,232 @@ +--- +import { getTldrCluster, getTldrCommandsByClusterPaginated, getTldrPage } from '../../../../db/tldrs/tldr-utils'; +import AdBanner from '../../../components/banner/AdBanner.astro'; +import Banner from '../../../components/banner/BannerIndex.astro'; +import CreditsButton from '../../../components/buttons/CreditsButton'; +import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; +import ToolContainer from '../../../components/tool/ToolContainer'; +import ToolHead from '../../../components/tool/ToolHead'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import TldrPlatform from '../_TldrPlatform.astro'; + +export const prerender = false; + +const { platform, slug } = Astro.params; +// console.log(`[TLDR_SLUG_PAGE] Start rendering ${platform}/${slug}`); +const startRender = Date.now(); + +if (!platform || !slug) { + return new Response(null, { status: 404 }); +} + +// Check if slug is a page number (pagination) or a command name +const isNumericPage = /^\d+$/.test(slug); +const pageNumber = isNumericPage ? parseInt(slug, 10) : null; +const command = !isNumericPage ? slug : null; + +let pageData: any = null; +let commandData: any = null; +let htmlContent: string = ''; + +if (pageNumber !== null) { + // Handle pagination route: /tldr/[platform]/[page] + if (pageNumber === 1) { + return Astro.redirect(`/freedevtools/tldr/${platform}/`); + } + + // Get cluster metadata first + const clusterData = await getTldrCluster(platform); + + if (!clusterData) { + return Astro.redirect('/404'); + } + + const itemsPerPage = 30; + const totalCommands = clusterData.count; + const totalPages = Math.ceil(totalCommands / itemsPerPage); + const currentPage = pageNumber; + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return Astro.redirect('/404'); + } + + const currentPageCommands = await getTldrCommandsByClusterPaginated( + platform, + currentPage, + itemsPerPage + ); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'TLDR', href: '/freedevtools/tldr/' }, + { label: platform, href: `/freedevtools/tldr/${platform}/` }, + { label: `Page ${currentPage}` }, + ]; + + // SEO data + const seoTitle = `${platform} Commands - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our ${platform} command documentation. Learn ${platform} commands quickly with practical examples.`; + const canonical = `https://hexmos.com/freedevtools/tldr/${platform}/${currentPage}/`; + + const paginatedPlatformKeywords = [ + `${platform} commands`, + `${platform} cli`, + `${platform} documentation`, + 'command line', + 'cli documentation', + 'terminal commands', + `page ${currentPage}`, + 'pagination', + 'command reference', + ]; + + pageData = { + currentPage, + totalPages, + totalCommands, + currentPageCommands, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + paginatedPlatformKeywords, + }; +} else if (command) { + // Handle command route: /tldr/[platform]/[command] + const startDb = Date.now(); + const page = await getTldrPage(platform, command); + const duration = Date.now() - startDb; + console.log(`[TLDR_SLUG_PAGE] getTldrPage(${platform}, ${command}) finished in ${duration}ms`); + + if ((Astro.locals as any).timings) { + (Astro.locals as any).timings['db fetch'] = duration; + } + + if (!page) { + return Astro.redirect('/404'); + } + + htmlContent = page.html_content || ''; + + const title = page.title || command; + const description = page.description || `Documentation for ${command} command`; + + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'TLDR', href: '/freedevtools/tldr/' }, + { label: platform, href: `/freedevtools/tldr/${platform}/` }, + { label: command }, + ]; + + commandData = { + page, + title, + description, + breadcrumbItems, + }; +} else { + return Astro.redirect('/404'); +} + +--- + +{pageData ? ( + + + +) : commandData ? ( + + + +
+ +
+ + +
+
+
+ + + +) : null} \ No newline at end of file diff --git a/frontend/src/pages/tldr/[platform]/index.astro b/frontend/src/pages/tldr/[platform]/index.astro index 64ee74cad6..f454cf3db9 100644 --- a/frontend/src/pages/tldr/[platform]/index.astro +++ b/frontend/src/pages/tldr/[platform]/index.astro @@ -1,93 +1,232 @@ --- -import { getCollection } from 'astro:content'; +import { + getAllTldrClusters, + getTldrCluster, + getTldrCommandsByClusterPaginated, +} from '../../../../db/tldrs/tldr-utils'; import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { getTldrPlatformCommands } from '../../../lib/tldr-utils'; +import Tldr from '../_Tldr.astro'; import TldrPlatform from '../_TldrPlatform.astro'; -// Test comment with bad spacing and formatting +export const prerender = false; -// Generate static paths for all platforms -export async function getStaticPaths() { - const tldrEntries = await getCollection('tldr'); - const platforms = new Set(); +const { platform } = Astro.params; + +if (!platform) { + return Astro.redirect('/404'); +} + +let paginationData: any = null; +let platformData: any = null; - for (const entry of tldrEntries) { - const pathParts = entry.id.split('/'); - const platform = pathParts[pathParts.length - 2]; - platforms.add(platform); +// If platform is numeric, this is actually a pagination route for the main index +// Render pagination content directly (workaround for route priority) +if (/^\d+$/.test(platform)) { + const currentPage = parseInt(platform, 10); + + // Redirect /tldr/1 to /tldr + if (currentPage === 1) { + return Astro.redirect('/freedevtools/tldr/'); } - return Array.from(platforms).map((platform) => ({ - params: { platform }, - })); -} + // Fetch all clusters to paginate manually + const start = Date.now(); + const allClusters = await getAllTldrClusters(); + console.log( + `[TLDR_PLATFORM_PAGE] getAllTldrClusters() finished in ${Date.now() - start}ms` + ); -const { platform } = Astro.params; + if (!allClusters) { + return Astro.redirect('/404'); + } + + const itemsPerPage = 30; + const totalPlatforms = allClusters.length; + const totalPages = Math.ceil(totalPlatforms / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return Astro.redirect('/404'); + } + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPagePlatforms = allClusters + .slice(startIndex, endIndex) + .map((c) => ({ + name: c.name, + count: c.count, + url: `/freedevtools/tldr/${c.name}/`, + commands: c.preview_commands, + })); + + const totalCommands = allClusters.reduce((acc, c) => acc + c.count, 0); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'TLDR', href: '/freedevtools/tldr/' }, + { label: `Page ${currentPage}` }, + ]; -// Get all commands for this platform -const allCommands = await getTldrPlatformCommands(platform!); - -// Pagination logic for page 1 -const itemsPerPage = 30; -const currentPage = 1; -const totalPages = Math.ceil(allCommands.length / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const commands = allCommands.slice(startIndex, endIndex); -const totalCommands = allCommands.length; - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'TLDR', href: '/freedevtools/tldr/' }, - { label: platform }, -]; - -// SEO data -const seoTitle = `${platform} Commands - TLDR Documentation | Online Free DevTools by Hexmos`; -const seoDescription = `Comprehensive documentation for ${platform} command-line tools. Learn ${platform} commands quickly with practical examples. ${totalCommands} commands available.`; -const canonical = `https://hexmos.com/freedevtools/tldr/${platform}/`; - -// Enhanced keywords for platform page -const platformKeywords = [ - `${platform} commands`, - `${platform} cli`, - `${platform} documentation`, - 'command line', - 'cli documentation', - 'terminal commands', - 'command reference', - 'cli documentation', -]; + // SEO data + const seoTitle = `TLDR - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our TLDR command documentation. Learn command-line tools across different platforms.`; + const canonical = `https://hexmos.com/freedevtools/tldr/${currentPage}/`; + + const paginatedKeywords = [ + 'tldr', + 'command line', + 'cli documentation', + 'terminal commands', + `page ${currentPage}`, + 'pagination', + 'command reference', + 'cli documentation', + ]; + + paginationData = { + currentPage, + totalPages, + totalPlatforms, + currentPagePlatforms, + totalCommands, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + paginatedKeywords, + itemsPerPage, + }; +} else { + // Get commands for this platform (Page 1) + const start = Date.now(); + + // Get cluster metadata first + const clusterData = await getTldrCluster(platform); + + if (!clusterData) { + return Astro.redirect('/404'); + } + + const itemsPerPage = 30; + const totalCommands = clusterData.count; + const totalPages = Math.ceil(totalCommands / itemsPerPage); + const currentPage = 1; + + // Get commands for page 1 + const commands = await getTldrCommandsByClusterPaginated( + platform, + currentPage, + itemsPerPage + ); + + console.log( + `[TLDR_PLATFORM_PAGE] getTldrCommandsByClusterPaginated(${platform}, 1) finished in ${Date.now() - start}ms` + ); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'TLDR', href: '/freedevtools/tldr/' }, + { label: platform }, + ]; + + // SEO data + const seoTitle = `${platform} Commands - TLDR Documentation | Online Free DevTools by Hexmos`; + const seoDescription = `Comprehensive documentation for ${platform} command-line tools. Learn ${platform} commands quickly with practical examples. ${totalCommands} commands available.`; + const canonical = `https://hexmos.com/freedevtools/tldr/${platform}/`; + + // Enhanced keywords for platform page + const platformKeywords = [ + `${platform} commands`, + `${platform} cli`, + `${platform} documentation`, + 'command line', + 'cli documentation', + 'terminal commands', + 'command reference', + 'cli documentation', + ]; + + platformData = { + platform, + commands, + currentPage, + totalPages, + totalCommands, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + platformKeywords, + itemsPerPage, + }; +} --- - - - +{ + paginationData ? ( + + + + ) : platformData ? ( + + + + ) : null +} diff --git a/frontend/src/pages/tldr/_Tldr.astro b/frontend/src/pages/tldr/_Tldr.astro index 2041907172..d21a8a78e6 100644 --- a/frontend/src/pages/tldr/_Tldr.astro +++ b/frontend/src/pages/tldr/_Tldr.astro @@ -12,168 +12,16 @@ import { StarIcon, } from '@radix-ui/react-icons'; +import { emojiMap } from '@/lib/tldr-constants'; + // Helper function to get platform emoji... const getPlatformEmoji = (platformName: string): string => { - const emojiMap: Record = { - android: '📱', - aws: '☁️', - bash: '🐚', - common: '🔧', - git: '📦', - linux: '🐧', - macos: '🍎', - node: '🟢', - python: '🐍', - windows: '🪟', - docker: '🐳', - kubernetes: '☸️', - terraform: '🏗️', - npm: '📦', - yarn: '🧶', - pnpm: '📦', - go: '🐹', - rust: '🦀', - php: '🐘', - ruby: '💎', - java: '☕', - csharp: '🔷', - cpp: '⚡', - c: '⚡', - swift: '🦉', - kotlin: '🟣', - scala: '🔴', - clojure: '🟢', - haskell: '🔷', - ocaml: '🟠', - fsharp: '🔵', - elixir: '💜', - erlang: '🔴', - lua: '🔵', - perl: '🐪', - r: '📊', - matlab: '🧮', - julia: '🔴', - dart: '🎯', - flutter: '🦋', - react: '⚛️', - vue: '💚', - angular: '🅰️', - svelte: '🧡', - next: '▲', - nuxt: '💚', - gatsby: '🌐', - webpack: '📦', - vite: '⚡', - rollup: '📦', - parcel: '📦', - babel: '🔧', - typescript: '🔷', - javascript: '🟨', - html: '🌐', - css: '🎨', - sass: '💅', - less: '🔷', - stylus: '💄', - postcss: '🔧', - tailwind: '🎨', - bootstrap: '🎨', - material: '🎨', - antd: '🎨', - chakra: '🎨', - mantine: '🎨', - semantic: '🎨', - bulma: '🎨', - foundation: '🎨', - pure: '🎨', - skeleton: '🎨', - milligram: '🎨', - spectre: '🎨', - uikit: '🎨', - materialize: '🎨', - vuetify: '🎨', - quasar: '🎨', - prime: '🎨', - element: '🎨', - iview: '🎨', - naive: '🎨', - arco: '🎨', - tdesign: '🎨', - nutui: '🎨', - vant: '🎨', - mint: '🎨', - cube: '🎨', - mand: '🎨', - weui: '🎨', - jquery: '🎨', - lodash: '🔧', - moment: '📅', - dayjs: '📅', - 'date-fns': '📅', - luxon: '📅', - ramda: '🔧', - underscore: '🔧', - immutable: '🔧', - mobx: '🔧', - redux: '🔧', - vuex: '🔧', - pinia: '🔧', - zustand: '🔧', - jotai: '🔧', - recoil: '🔧', - swr: '🔧', - 'react-query': '🔧', - apollo: '🔧', - graphql: '🔧', - prisma: '🔧', - sequelize: '🔧', - mongoose: '🔧', - typeorm: '🔧', - knex: '🔧', - bookshelf: '🔧', - waterline: '🔧', - sails: '🔧', - loopback: '🔧', - feathers: '🔧', - hapi: '🔧', - fastify: '🔧', - koa: '🔧', - express: '🔧', - connect: '🔧', - restify: '🔧', - polka: '🔧', - micro: '🔧', - vercel: '🔧', - netlify: '🔧', - heroku: '🔧', - railway: '🔧', - render: '🔧', - fly: '🔧', - digitalocean: '☁️', - linode: '☁️', - vultr: '☁️', - azure: '☁️', - gcp: '☁️', - ibm: '☁️', - oracle: '☁️', - alibaba: '☁️', - tencent: '☁️', - baidu: '☁️', - huawei: '☁️', - rackspace: '☁️', - joyent: '☁️', - softlayer: '☁️', - ovh: '☁️', - scaleway: '☁️', - hetzner: '☁️', - contabo: '☁️', - }; - return emojiMap[platformName.toLowerCase()] || '🔧'; }; // Helper function to render platform card function renderPlatformCard(platform: any, platformCommands: any[]): string { - const commandsHtml = platformCommands + const commandsHtml = (platformCommands || []) .slice(0, 3) .map( (command) => @@ -229,16 +77,10 @@ const { breadcrumbItems, } = Astro.props; -// Get commands for each platform import AdBanner from '../../components/banner/AdBanner.astro'; -import { getTldrPlatformCommands } from '../../lib/tldr-utils'; -const platformsWithCommands = await Promise.all( - platforms.map(async (platform: any) => { - const commands = await getTldrPlatformCommands(platform.name); - return { ...platform, commands }; - }) -); +// Platforms already have commands attached from the parent component +const platformsWithCommands = platforms; --- diff --git a/frontend/src/pages/tldr/_TldrPlatform.astro b/frontend/src/pages/tldr/_TldrPlatform.astro index d11c42f48b..a104226003 100644 --- a/frontend/src/pages/tldr/_TldrPlatform.astro +++ b/frontend/src/pages/tldr/_TldrPlatform.astro @@ -91,7 +91,6 @@ const {
diff --git a/frontend/src/pages/tldr/_correct_frontmatter.py b/frontend/src/pages/tldr/_correct_frontmatter.py deleted file mode 100644 index 74d356fc14..0000000000 --- a/frontend/src/pages/tldr/_correct_frontmatter.py +++ /dev/null @@ -1,452 +0,0 @@ -#!/usr/bin/env python3 - -import os -import sys -import time - -import requests - -API_KEYS = ["AIzaSyADNt8eQc93cR7CmeBhOe-9oqkzbvSm9J8"] -API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent" - -OG_IMAGE = "https://hexmos.com/freedevtools/site-banner.png" -TWITTER_IMAGE = "https://hexmos.com/freedevtools/site-banner.png" - - -def validate_seo_compliance(yaml_content, platform, command_name): - """Validate SEO compliance and return issues""" - issues = [] - lines = yaml_content.split("\n") - - for line in lines: - line = line.strip() - - # Check title length and format - if line.startswith("title:"): - title = line.split(":", 1)[1].strip().strip("\"'") - if len(title) < 50 or len(title) > 60: - issues.append(f"Title length {len(title)} chars (should be 50-60)") - if not title.endswith("| Online Free DevTools by Hexmos"): - issues.append( - "Title missing brand suffix '| Online Free DevTools by Hexmos'" - ) - if not any( - word in title.lower() - for word in [ - "control", - "generate", - "format", - "validate", - "create", - "manage", - "convert", - "command", - "syntax", - "reference", - "examples", - ] - ): - issues.append("Title missing action word") - - # Check description length and format - elif line.startswith("description:"): - desc = line.split(":", 1)[1].strip().strip("\"'") - if len(desc) < 140 or len(desc) > 160: - issues.append( - f"Description length {len(desc)} chars (should be 140-160)" - ) - if not any( - phrase in desc.lower() - for phrase in [ - "free online tool", - "no registration", - "instantly", - "easily", - "command reference", - "syntax guide", - "examples", - ] - ): - issues.append("Description missing call-to-action") - - return issues - - -def enhance_seo_keywords(keywords, platform, command_name): - """Enhance keywords based on platform and command""" - enhanced = [] - - # Platform-specific keyword mapping - platform_keywords = { - "android": ["adb", "android development", "mobile debugging"], - "linux": ["linux command", "unix", "terminal", "bash"], - "macos": ["macos command", "osx", "terminal", "zsh"], - "windows": ["windows command", "powershell", "cmd", "dos"], - "freebsd": ["freebsd command", "bsd", "unix"], - "ubuntu": ["ubuntu command", "debian", "apt"], - "centos": ["centos command", "rhel", "yum"], - "fedora": ["fedora command", "dnf", "rpm"], - "arch": ["arch command", "pacman", "aur"], - "alpine": ["alpine command", "apk", "musl"], - "common": ["command line", "terminal", "cli"], - } - - # Command-specific keyword mapping - command_keywords = { - "am": ["activity manager", "android activities", "intent management"], - "base64": ["base64 encoding", "data encoding", "text encoding"], - "grep": ["text search", "pattern matching", "file search"], - "find": ["file search", "directory search", "file location"], - "ls": ["directory listing", "file listing", "directory contents"], - "ps": ["process management", "running processes", "system processes"], - "top": ["system monitoring", "process monitoring", "resource usage"], - "kill": ["process termination", "kill process", "stop process"], - "chmod": ["file permissions", "permission management", "access control"], - "chown": ["file ownership", "ownership management", "user management"], - "zstd": ["zstd command", "zstd syntax", "zstd examples", "zstandard commands"], - } - - # Add platform-specific keywords - if platform in platform_keywords: - enhanced.extend(platform_keywords[platform]) - - # Add command-specific keywords - if command_name in command_keywords: - enhanced.extend(command_keywords[command_name]) - - # Add existing keywords - enhanced.extend(keywords) - - # Remove duplicates and limit to 10 - return list(dict.fromkeys(enhanced))[:10] - - -def normalize_yaml_format(yaml_content, platform, command_name): - """Convert array syntax to proper YAML list format and fix path format""" - lines = yaml_content.split("\n") - normalized_lines = [] - canonical_added = False - keywords_section = False - current_keywords = [] - - i = 0 - while i < len(lines): - line = lines[i].strip() - - # Check if this line has array syntax for keywords or features - if line.startswith("keywords:") and "[" in line: - # Extract array content - array_start = line.find("[") - array_end = line.find("]") - if array_start != -1 and array_end != -1: - array_content = line[array_start + 1 : array_end] - # Split by comma and clean up - items = [item.strip().strip("'\"") for item in array_content.split(",")] - normalized_lines.append("keywords:") - for item in items: - if item: # Skip empty items - current_keywords.append(item) - normalized_lines.append(f" - {item}") - else: - normalized_lines.append(line) - keywords_section = True - elif line.startswith("features:") and "[" in line: - # Extract array content - array_start = line.find("[") - array_end = line.find("]") - if array_start != -1 and array_end != -1: - array_content = line[array_start + 1 : array_end] - # Split by comma and clean up - items = [item.strip().strip("'\"") for item in array_content.split(",")] - normalized_lines.append("features:") - for item in items: - if item: # Skip empty items - normalized_lines.append(f" - {item}") - else: - normalized_lines.append(line) - elif line.startswith("path:"): - # Fix path format to include /freedevtools/tldr/ prefix - path_value = line.split(":", 1)[1].strip().strip("\"'") - if not path_value.startswith("/freedevtools/tldr/"): - if path_value.startswith("/"): - path_value = path_value[1:] # Remove leading slash - if not path_value.startswith("freedevtools/tldr/"): - path_value = f"freedevtools/tldr/{path_value}" - normalized_lines.append(f'path: "/{path_value}"') - else: - normalized_lines.append(line) - # Add canonical after path - if not canonical_added: - canonical_url = ( - f"https://hexmos.com/freedevtools/tldr/{platform}/{command_name}/" - ) - normalized_lines.append(f'canonical: "{canonical_url}"') - canonical_added = True - elif line.startswith("canonical:"): - # Skip any existing canonical lines from AI - pass - elif keywords_section and line.startswith("- "): - # Collect keywords for enhancement - current_keywords.append(line[2:].strip()) - normalized_lines.append(line) - elif keywords_section and not line.startswith("- ") and line: - # End of keywords section, enhance them - if current_keywords: - enhanced_keywords = enhance_seo_keywords( - current_keywords, platform, command_name - ) - # Replace the keywords section - normalized_lines = [ - l for l in normalized_lines if not l.startswith(" - ") - ] - # Remove the keywords: line - normalized_lines = [ - l for l in normalized_lines if not l.startswith("keywords:") - ] - # Add enhanced keywords - normalized_lines.append("keywords:") - for keyword in enhanced_keywords: - normalized_lines.append(f" - {keyword}") - keywords_section = False - normalized_lines.append(line) - else: - normalized_lines.append(line) - i += 1 - - # If canonical wasn't added yet, add it at the end - if not canonical_added: - canonical_url = ( - f"https://hexmos.com/freedevtools/tldr/{platform}/{command_name}/" - ) - normalized_lines.append(f'canonical: "{canonical_url}"') - - return "\n".join(normalized_lines) - - -def send_to_gemini(md_content, platform, command_name, api_key): - prompt = f"""Correct and improve the SEO-optimized frontmatter metadata in YAML format for the following markdown content: - -Platform: {platform} -Command: {command_name} - -{md_content} - -SEO Requirements (CRITICAL): -- Include: title, name, path, description, category, keywords, features -- Use proper YAML list format with dashes (-) for keywords and features, NOT array syntax - -TITLE Requirements: -- Format: "[Tool Name] - [Primary Function] | Online Free DevTools by Hexmos" -- Length: 50-60 characters -- Primary keyword in first 3 words -- Use action words like "Control", "Generate", "Format", "Validate", "Create", "Command", "Syntax", "Reference" -- Include brand "Free DevTools" at the end - -DESCRIPTION Requirements: -- Format: "[Primary keyword action] with [Tool Name]" -- Length: 140-160 characters -- Contains primary keyword -- Contains 1 secondary keyword -- Has clear call-to-action like "Free online tool, no registration required" or "Command reference and examples" - -KEYWORDS Requirements: -- Generate 10 SEO-friendly keywords following formula: [Data/Format Type] + [Action/Tool Type] -- Primary keyword should be the main search term users type -- Include platform-specific keywords (e.g., "adb", "android", "linux", "macos") -- Include tool-specific keywords (e.g., "activity manager", "file converter", "password generator") -- No generic words like "tool", "helper", "utility" -- No keyword stuffing - natural distribution - -FEATURES Requirements: -- Generate 5 specific capabilities that describe what the command can do -- Use action-oriented language -- Be specific about functionality - -OTHER Requirements: -- Use lowercase for category (use the platform name as category) -- Path should be in the format "/freedevtools/tldr/{platform}/{command_name}" -- Do NOT include canonical field - it will be added automatically -- Return ONLY the YAML content without any code block markers (no ```yaml or ```) - -Example format: -title: "Android Activity Manager - Control App Activities with ADB | Online Free DevTools by Hexmos" -name: {command_name} -path: /freedevtools/tldr/{platform}/{command_name} -description: "Control Android app activities instantly with ADB Activity Manager. Start activities, manage intents, and debug applications using command line. Free online tool, no registration required." -category: {platform} -keywords: - - descriptive keyword phrase 1 - - descriptive keyword phrase 2 - - descriptive keyword phrase 3 - - descriptive keyword phrase 4 - - descriptive keyword phrase 5 - - descriptive keyword phrase 6 - - descriptive keyword phrase 7 - - descriptive keyword phrase 8 - - descriptive keyword phrase 9 - - descriptive keyword phrase 10 -features: - - specific capability 1 - - specific capability 2 - - specific capability 3 - - specific capability 4 - - specific capability 5""" - - headers = {"Content-Type": "application/json", "X-goog-api-key": api_key} - data = {"contents": [{"parts": [{"text": prompt}]}]} - response = requests.post(API_URL, headers=headers, json=data) - if response.status_code == 200: - try: - result = response.json() - generated_text = result["candidates"][0]["content"]["parts"][0]["text"] - # Strip any code block markers that might be present - generated_text = generated_text.strip() - if generated_text.startswith("```yaml"): - generated_text = generated_text[7:] # Remove ```yaml - if generated_text.startswith("```"): - generated_text = generated_text[3:] # Remove ``` - if generated_text.endswith("```"): - generated_text = generated_text[:-3] # Remove trailing ``` - - # Post-process to ensure consistent YAML formatting - generated_text = normalize_yaml_format( - generated_text, platform, command_name - ) - - # Validate SEO compliance - seo_issues = validate_seo_compliance(generated_text, platform, command_name) - if seo_issues: - print(f"SEO Issues for {command_name}: {', '.join(seo_issues)}") - - return generated_text.strip() - except Exception as e: - print("Failed to parse Gemini response:", e) - return None - else: - print(f"Error from Gemini API: {response.status_code} {response.text}") - return None - - -def update_markdown_file(file_path, frontmatter_yaml): - try: - with open(file_path, "r") as f: - content = f.read() - - if content.startswith("---"): - parts = content.split("---", 2) - if len(parts) >= 3: - content = parts[2].lstrip() - - new_content = f"---\n{frontmatter_yaml.strip()}\n---\n\n{content}" - - with open(file_path, "w") as f: - f.write(new_content) - except Exception as e: - print(f"Error updating file {file_path}: {e}") - - -def process_file(md_file, api_key): - try: - with open(md_file, "r") as f: - content = f.read() - - name = os.path.splitext(os.path.basename(md_file))[0] - - # Extract platform from the file path - # Platform is the parent directory of the markdown file - path_parts = md_file.split("/") - - # Find the platform directory (should be the directory containing the .md file) - platform = "unknown" - for i, part in enumerate(path_parts): - if part.endswith(".md"): - # The platform is the directory before the .md file - if i > 0: - platform = path_parts[i - 1] - break - - print(f"Processing {name} from {platform}...") - - frontmatter_yaml = send_to_gemini(content, platform, name, api_key) - if frontmatter_yaml: - update_markdown_file(md_file, frontmatter_yaml) - print(f"✅ Updated {name}") - return True - else: - print(f"❌ Failed to generate frontmatter for {name}") - return False - except Exception as e: - print(f"❌ Error processing {md_file}: {e}") - return False - - -def read_csv_file(csv_path): - """Read file paths from CSV file""" - file_paths = [] - try: - with open(csv_path, "r") as f: - for line in f: - line = line.strip() - if line and not line.startswith("#"): # Skip empty lines and comments - file_paths.append(line) - return file_paths - except Exception as e: - print(f"Error reading CSV file {csv_path}: {e}") - return [] - - -def main(): - # Hardcoded input file path - csv_path = os.path.expanduser("~/hex/freedevtools/frontend/input.csv") - - if not os.path.exists(csv_path): - print(f"CSV file not found: {csv_path}") - sys.exit(1) - - print(f"Reading file paths from: {csv_path}") - file_paths = read_csv_file(csv_path) - - if not file_paths: - print("No file paths found in CSV") - sys.exit(1) - - print(f"Found {len(file_paths)} files to process") - - if not API_KEYS: - print("No API keys configured. Please add API keys to the API_KEYS list.") - sys.exit(1) - - # Use the first API key - api_key = API_KEYS[0] - - success_count = 0 - total_count = len(file_paths) - - for i, file_path in enumerate(file_paths): - print(f"\n[{i+1}/{total_count}] Processing: {file_path}") - - if not os.path.exists(file_path): - print(f"❌ File not found: {file_path}") - continue - - if process_file(file_path, api_key): - success_count += 1 - - # Small delay to avoid rate limiting - time.sleep(1) - - print(f"\n🎉 Processing completed!") - print(f"✅ Successfully processed: {success_count}/{total_count} files") - - -if __name__ == "__main__": - print("Script starting...") - print(f"Available API keys: {len(API_KEYS)}") - try: - main() - print("Script completed successfully.") - except Exception as e: - print(f"Script failed with error: {e}") - import traceback - - traceback.print_exc() diff --git a/frontend/src/pages/tldr/_find_paths.py b/frontend/src/pages/tldr/_find_paths.py deleted file mode 100644 index 4840301194..0000000000 --- a/frontend/src/pages/tldr/_find_paths.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to recursively find markdown files in tldr directory and extract frontmatter information. -Outputs: path | filename | frontmatter-path | frontmatter-canonical -""" - -import os -import re -import yaml -from pathlib import Path - -def extract_frontmatter(file_path): - """Extract frontmatter from a markdown file.""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Check if file starts with frontmatter - if not content.startswith('---'): - return None, None - - # Find the end of frontmatter - end_marker = content.find('---', 3) - if end_marker == -1: - return None, None - - # Extract frontmatter content - frontmatter_content = content[3:end_marker].strip() - - # Parse YAML - try: - frontmatter = yaml.safe_load(frontmatter_content) - path = frontmatter.get('path', '') - canonical = frontmatter.get('canonical', '') - return path, canonical - except yaml.YAMLError: - return None, None - - except Exception as e: - print(f"Error reading {file_path}: {e}") - return None, None - -def fix_frontmatter_paths(file_path, old_path, old_canonical, new_path, new_canonical): - """Fix the frontmatter paths and canonical URL in a markdown file using simple string replacement.""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Simple string replacement - new_content = content.replace(old_path, new_path) - new_content = new_content.replace(old_canonical, new_canonical) - - # Write back to file - with open(file_path, 'w', encoding='utf-8') as f: - f.write(new_content) - - return True - - except Exception as e: - print(f"Error fixing {file_path}: {e}") - return False - -def find_md_files(root_dir): - """Recursively find all markdown files and extract required information.""" - root_path = Path(root_dir) - - if not root_path.exists(): - print(f"Directory {root_dir} does not exist") - return - - # Find all .md files recursively - md_files = list(root_path.rglob('*.md')) - - for md_file in sorted(md_files): - # Get relative path from tldr directory - relative_path = md_file.relative_to(root_path) - - # Get directory path (without filename) - dir_path = relative_path.parent - - # Get filename without extension - filename = md_file.stem - - # Extract frontmatter - frontmatter_path, frontmatter_canonical = extract_frontmatter(md_file) - - if frontmatter_path and frontmatter_canonical: - # Check if the frontmatter path matches the expected pattern - # Expected pattern: /freedevtools/tldr/{dir_path}/{filename} - expected_path = f"/freedevtools/tldr/{dir_path}/{filename}" - expected_canonical = f"https://hexmos.com/freedevtools/tldr/{dir_path}/{filename}/" - - # Only process if there's a mismatch - if frontmatter_path != expected_path: - print(f"Fixing: {dir_path} | {filename}.md") - print(f" Old path: {frontmatter_path}") - print(f" New path: {expected_path}") - print(f" Old canonical: {frontmatter_canonical}") - print(f" New canonical: {expected_canonical}") - - # Fix the file - fix_frontmatter_paths(md_file, frontmatter_path, frontmatter_canonical, expected_path, expected_canonical) - print(" ✓ Fixed!") - print() - -def main(): - # Get the directory where this script is located - script_dir = Path(__file__).parent - tldr_dir = '/home/lovestaco/hex/freedevtools/frontend/src/pages/markdown_pages/tldr' - - print("Finding markdown files in:", tldr_dir) - print("=" * 80) - - find_md_files(tldr_dir) - -if __name__ == "__main__": - main() diff --git a/frontend/src/pages/tldr/_generate_frontmatter.py b/frontend/src/pages/tldr/_generate_frontmatter.py deleted file mode 100644 index 3007d84b73..0000000000 --- a/frontend/src/pages/tldr/_generate_frontmatter.py +++ /dev/null @@ -1,520 +0,0 @@ -#!/usr/bin/env python3 - -import json -import os -import sys -import time -from concurrent.futures import ThreadPoolExecutor, as_completed - -import requests - -API_KEYS = [] -API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent" - -OG_IMAGE = "https://hexmos.com/freedevtools/site-banner.png" -TWITTER_IMAGE = "https://hexmos.com/freedevtools/site-banner.png" - - -def validate_seo_compliance(yaml_content, platform, command_name): - """Validate SEO compliance and return issues""" - issues = [] - lines = yaml_content.split("\n") - - for line in lines: - line = line.strip() - - # Check title length and format - if line.startswith("title:"): - title = line.split(":", 1)[1].strip().strip("\"'") - if len(title) < 50 or len(title) > 60: - issues.append(f"Title length {len(title)} chars (should be 50-60)") - if not title.endswith("| Online Free DevTools by Hexmos"): - issues.append( - "Title missing brand suffix '| Online Free DevTools by Hexmos'" - ) - if not any( - word in title.lower() - for word in [ - "control", - "generate", - "format", - "validate", - "create", - "manage", - "convert", - ] - ): - issues.append("Title missing action word") - - # Check description length and format - elif line.startswith("description:"): - desc = line.split(":", 1)[1].strip().strip("\"'") - if len(desc) < 140 or len(desc) > 160: - issues.append( - f"Description length {len(desc)} chars (should be 140-160)" - ) - if not any( - phrase in desc.lower() - for phrase in [ - "free online tool", - "no registration", - "instantly", - "easily", - ] - ): - issues.append("Description missing call-to-action") - - return issues - - -def enhance_seo_keywords(keywords, platform, command_name): - """Enhance keywords based on platform and command""" - enhanced = [] - - # Platform-specific keyword mapping - platform_keywords = { - "android": ["adb", "android development", "mobile debugging"], - "linux": ["linux command", "unix", "terminal", "bash"], - "macos": ["macos command", "osx", "terminal", "zsh"], - "windows": ["windows command", "powershell", "cmd", "dos"], - "freebsd": ["freebsd command", "bsd", "unix"], - "ubuntu": ["ubuntu command", "debian", "apt"], - "centos": ["centos command", "rhel", "yum"], - "fedora": ["fedora command", "dnf", "rpm"], - "arch": ["arch command", "pacman", "aur"], - "alpine": ["alpine command", "apk", "musl"], - } - - # Command-specific keyword mapping - command_keywords = { - "am": ["activity manager", "android activities", "intent management"], - "base64": ["base64 encoding", "data encoding", "text encoding"], - "grep": ["text search", "pattern matching", "file search"], - "find": ["file search", "directory search", "file location"], - "ls": ["directory listing", "file listing", "directory contents"], - "ps": ["process management", "running processes", "system processes"], - "top": ["system monitoring", "process monitoring", "resource usage"], - "kill": ["process termination", "kill process", "stop process"], - "chmod": ["file permissions", "permission management", "access control"], - "chown": ["file ownership", "ownership management", "user management"], - } - - # Add platform-specific keywords - if platform in platform_keywords: - enhanced.extend(platform_keywords[platform]) - - # Add command-specific keywords - if command_name in command_keywords: - enhanced.extend(command_keywords[command_name]) - - # Add existing keywords - enhanced.extend(keywords) - - # Remove duplicates and limit to 10 - return list(dict.fromkeys(enhanced))[:10] - - -def normalize_yaml_format(yaml_content, platform, command_name): - """Convert array syntax to proper YAML list format and fix path format""" - lines = yaml_content.split("\n") - normalized_lines = [] - canonical_added = False - keywords_section = False - current_keywords = [] - - i = 0 - while i < len(lines): - line = lines[i].strip() - - # Check if this line has array syntax for keywords or features - if line.startswith("keywords:") and "[" in line: - # Extract array content - array_start = line.find("[") - array_end = line.find("]") - if array_start != -1 and array_end != -1: - array_content = line[array_start + 1 : array_end] - # Split by comma and clean up - items = [item.strip().strip("'\"") for item in array_content.split(",")] - normalized_lines.append("keywords:") - for item in items: - if item: # Skip empty items - current_keywords.append(item) - normalized_lines.append(f" - {item}") - else: - normalized_lines.append(line) - keywords_section = True - elif line.startswith("features:") and "[" in line: - # Extract array content - array_start = line.find("[") - array_end = line.find("]") - if array_start != -1 and array_end != -1: - array_content = line[array_start + 1 : array_end] - # Split by comma and clean up - items = [item.strip().strip("'\"") for item in array_content.split(",")] - normalized_lines.append("features:") - for item in items: - if item: # Skip empty items - normalized_lines.append(f" - {item}") - else: - normalized_lines.append(line) - elif line.startswith("path:"): - # Fix path format to include /freedevtools/tldr/ prefix - path_value = line.split(":", 1)[1].strip().strip("\"'") - if not path_value.startswith("/freedevtools/tldr/"): - if path_value.startswith("/"): - path_value = path_value[1:] # Remove leading slash - if not path_value.startswith("freedevtools/tldr/"): - path_value = f"freedevtools/tldr/{path_value}" - normalized_lines.append(f'path: "/{path_value}"') - else: - normalized_lines.append(line) - # Add canonical after path - if not canonical_added: - canonical_url = ( - f"https://hexmos.com/freedevtools/tldr/{platform}/{command_name}/" - ) - normalized_lines.append(f'canonical: "{canonical_url}"') - canonical_added = True - elif line.startswith("canonical:"): - # Skip any existing canonical lines from AI - pass - elif keywords_section and line.startswith("- "): - # Collect keywords for enhancement - current_keywords.append(line[2:].strip()) - normalized_lines.append(line) - elif keywords_section and not line.startswith("- ") and line: - # End of keywords section, enhance them - if current_keywords: - enhanced_keywords = enhance_seo_keywords( - current_keywords, platform, command_name - ) - # Replace the keywords section - normalized_lines = [ - l for l in normalized_lines if not l.startswith(" - ") - ] - # Remove the keywords: line - normalized_lines = [ - l for l in normalized_lines if not l.startswith("keywords:") - ] - # Add enhanced keywords - normalized_lines.append("keywords:") - for keyword in enhanced_keywords: - normalized_lines.append(f" - {keyword}") - keywords_section = False - normalized_lines.append(line) - else: - normalized_lines.append(line) - i += 1 - - # If canonical wasn't added yet, add it at the end - if not canonical_added: - canonical_url = ( - f"https://hexmos.com/freedevtools/tldr/{platform}/{command_name}/" - ) - normalized_lines.append(f'canonical: "{canonical_url}"') - - return "\n".join(normalized_lines) - - -def find_markdown_files(root_dir): - md_files = [] - for dirpath, _, filenames in os.walk(root_dir): - for filename in filenames: - if filename.endswith(".md"): - md_files.append(os.path.join(dirpath, filename)) - return md_files - - -def send_to_gemini(md_content, platform, command_name, api_key): - prompt = f"""Generate SEO-optimized frontmatter metadata in YAML format for the following markdown content: - -Platform: {platform} -Command: {command_name} - -{md_content} - -SEO Requirements (CRITICAL): -- Include: title, name, path, description, category, keywords, features -- Use proper YAML list format with dashes (-) for keywords and features, NOT array syntax - -TITLE Requirements: -- Format: "[Tool Name] - [Primary Function] | Online Free DevTools by Hexmos" -- Length: 50-60 characters -- Primary keyword in first 3 words -- Use action words like "Control", "Generate", "Format", "Validate", "Create" -- Include brand "Free DevTools" at the end - -DESCRIPTION Requirements: -- Format: "[Primary keyword action] with [Tool Name]" -- Length: 140-160 characters -- Contains primary keyword -- Contains 1 secondary keyword -- Has clear call-to-action like "Free online tool, no registration required" - -KEYWORDS Requirements: -- Generate 10 SEO-friendly keywords following formula: [Data/Format Type] + [Action/Tool Type] -- Primary keyword should be the main search term users type -- Include platform-specific keywords (e.g., "adb", "android", "linux", "macos") -- Include tool-specific keywords (e.g., "activity manager", "file converter", "password generator") -- No generic words like "tool", "helper", "utility" -- No keyword stuffing - natural distribution - -FEATURES Requirements: -- Generate 5 specific capabilities that describe what the command can do -- Use action-oriented language -- Be specific about functionality - -OTHER Requirements: -- Use lowercase for category (use the platform name as category) -- Path should be in the format "/freedevtools/tldr/{platform}/{command_name}" -- Do NOT include canonical field - it will be added automatically -- Return ONLY the YAML content without any code block markers (no ```yaml or ```) - -Example format: -title: "Android Activity Manager - Control App Activities with ADB | Online Free DevTools by Hexmos" -name: {command_name} -path: /freedevtools/tldr/{platform}/{command_name} -description: "Control Android app activities instantly with ADB Activity Manager. Start activities, manage intents, and debug applications using command line. Free online tool, no registration required." -category: {platform} -keywords: - - descriptive keyword phrase 1 - - descriptive keyword phrase 2 - - descriptive keyword phrase 3 - - descriptive keyword phrase 4 - - descriptive keyword phrase 5 - - descriptive keyword phrase 6 - - descriptive keyword phrase 7 - - descriptive keyword phrase 8 - - descriptive keyword phrase 9 - - descriptive keyword phrase 10 -features: - - specific capability 1 - - specific capability 2 - - specific capability 3 - - specific capability 4 - - specific capability 5""" - - headers = {"Content-Type": "application/json", "X-goog-api-key": api_key} - data = {"contents": [{"parts": [{"text": prompt}]}]} - response = requests.post(API_URL, headers=headers, json=data) - if response.status_code == 200: - try: - result = response.json() - generated_text = result["candidates"][0]["content"]["parts"][0]["text"] - # Strip any code block markers that might be present - generated_text = generated_text.strip() - if generated_text.startswith("```yaml"): - generated_text = generated_text[7:] # Remove ```yaml - if generated_text.startswith("```"): - generated_text = generated_text[3:] # Remove ``` - if generated_text.endswith("```"): - generated_text = generated_text[:-3] # Remove trailing ``` - - # Post-process to ensure consistent YAML formatting - generated_text = normalize_yaml_format( - generated_text, platform, command_name - ) - - # Validate SEO compliance - seo_issues = validate_seo_compliance(generated_text, platform, command_name) - if seo_issues: - print(f"SEO Issues for {command_name}: {', '.join(seo_issues)}") - - return generated_text.strip() - except Exception as e: - print("Failed to parse Gemini response:", e) - return None - else: - print(f"Error from Gemini API: {response.status_code} {response.text}") - return None - - -def update_markdown_file(file_path, frontmatter_yaml): - try: - with open(file_path, "r") as f: - content = f.read() - - if content.startswith("---"): - parts = content.split("---", 2) - if len(parts) >= 3: - content = parts[2].lstrip() - - new_content = f'---\n{frontmatter_yaml.strip()}\nogImage: "{OG_IMAGE}"\ntwitterImage: "{TWITTER_IMAGE}"\n---\n\n{content}' - - with open(file_path, "w") as f: - f.write(new_content) - except Exception as e: - print(f"Error updating file {file_path}: {e}") - - -def already_processed(entries, name, path): - for entry in entries: - if entry.get("name") == name or entry.get("path") == path: - return True - return False - - -def update_frontmatter_json(entries, name, path): - entries.append({"name": name, "path": path}) - save_frontmatter(entries) - - -def process_file(md_file, base_path, api_key, process_id, file_index, total_files): - try: - with open(md_file, "r") as f: - content = f.read() - - name = os.path.splitext(os.path.basename(md_file))[0] - path = md_file.replace(base_path, "").replace("\\", "/") - if not path.startswith("/"): - path = "/" + path - path = path.replace(".md", "/") - - # Extract platform from the file path - # Platform is the parent directory of the markdown file - relative_path = md_file.replace(base_path, "").replace("\\", "/").strip("/") - path_parts = relative_path.split("/") - - # Find the platform directory (should be the directory containing the .md file) - platform = "unknown" - for i, part in enumerate(path_parts): - if part.endswith(".md"): - # The platform is the directory before the .md file - if i > 0: - platform = path_parts[i - 1] - break - - frontmatter_yaml = send_to_gemini(content, platform, name, api_key) - if frontmatter_yaml: - update_markdown_file(md_file, frontmatter_yaml) - return {"status": "success", "file": md_file, "platform": platform} - else: - return { - "status": "failed", - "file": md_file, - "reason": "no frontmatter returned", - } - except Exception as e: - return {"status": "error", "file": md_file, "error": str(e)} - - -def process_batch(batch_files, base_path, api_key, process_id): - """Process a batch of files with a single API key""" - print(f"Process {process_id} started with {len(batch_files)} files") - - for i, md_file in enumerate(batch_files): - print( - f"Process {process_id}: Processing {i+1}/{len(batch_files)} - {os.path.basename(md_file)}" - ) - result = process_file( - md_file, base_path, api_key, process_id, i + 1, len(batch_files) - ) - - if result["status"] == "success": - print( - f"Process {process_id}: ✅ {os.path.basename(md_file)} - {result['platform']}" - ) - elif result["status"] == "skipped": - print( - f"Process {process_id}: ⏭️ {os.path.basename(md_file)} - already processed" - ) - else: - print( - f"Process {process_id}: ❌ {os.path.basename(md_file)} - {result.get('reason', result.get('error', 'unknown error'))}" - ) - - # Small delay to avoid rate limiting - time.sleep(1) - - print(f"Process {process_id} completed") - - -def main(): - if len(sys.argv) != 2: - print("Usage: generate_frontmatter.py ") - sys.exit(1) - - root_dir = sys.argv[1] - print(f"Searching for markdown files in: {root_dir}") - md_files = find_markdown_files(root_dir) - print(f"Found {len(md_files)} markdown files") - - # Filter out files that already have frontmatter (--- markers) - unprocessed_files = [] - for md_file in md_files: - # Check if file already has frontmatter - try: - with open(md_file, "r") as f: - content = f.read() - - # Count --- markers - dash_count = content.count("---") - if dash_count >= 2: - print( - f"⏭️ Skipping {os.path.basename(md_file)} - already has frontmatter ({dash_count} --- markers)" - ) - continue - - except Exception as e: - print(f"⚠️ Could not read {os.path.basename(md_file)}: {e}") - continue - - unprocessed_files.append(md_file) - - print(f"Found {len(unprocessed_files)} unprocessed files") - - if not unprocessed_files: - print("All files have been processed!") - return - - # Calculate batch size (split by 5 processes, max 10 files per process) - total_files = len(unprocessed_files) - num_processes = min(5, len(API_KEYS)) - batch_size = max(1, total_files // num_processes) - - print( - f"Processing {total_files} files in {num_processes} batches of ~{batch_size} files each" - ) - print(f"Using {len(API_KEYS)} API keys") - - # Create batches - batches = [] - for i in range(0, len(unprocessed_files), batch_size): - batch = unprocessed_files[i : i + batch_size] - batches.append(batch) - - # Ensure we don't have more batches than API keys - batches = batches[: len(API_KEYS)] - - # Process batches in parallel - print(f"\n🚀 Starting {len(batches)} parallel processes...") - - with ThreadPoolExecutor(max_workers=len(batches)) as executor: - # Submit all batches - futures = [] - for i, batch in enumerate(batches): - api_key = API_KEYS[i % len(API_KEYS)] - future = executor.submit(process_batch, batch, root_dir, api_key, i + 1) - futures.append(future) - - # Wait for all processes to complete - for future in as_completed(futures): - try: - future.result() - except Exception as e: - print(f"❌ Process failed: {e}") - - print("\n🎉 All processes completed!") - - -if __name__ == "__main__": - print("Script starting...") - print(f"Available API keys: {len(API_KEYS)}") - try: - main() - print("Script completed successfully.") - except Exception as e: - print(f"Script failed with error: {e}") - import traceback - - traceback.print_exc() diff --git a/frontend/src/pages/tldr/_tldr_pages_overview.py b/frontend/src/pages/tldr/_tldr_pages_overview.py deleted file mode 100644 index cc710f83df..0000000000 --- a/frontend/src/pages/tldr/_tldr_pages_overview.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to recursively find markdown files in tldr directory and extract frontmatter information. -Outputs a JSON file containing name, description, and path for each TLDR page. -""" - -import os -import yaml -import json -from pathlib import Path - -def extract_frontmatter_data(file_path): - """Extract name, description, and path from a markdown file's frontmatter.""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Check if file starts with frontmatter - if not content.startswith('---'): - return None - - # Find the end of frontmatter - end_marker = content.find('---', 3) - if end_marker == -1: - return None - - # Extract frontmatter content - frontmatter_content = content[3:end_marker].strip() - - # Parse YAML - try: - frontmatter = yaml.safe_load(frontmatter_content) - # Get only the requested fields - return { - 'name': frontmatter.get('name', ''), - 'description': frontmatter.get('description', ''), - 'path': frontmatter.get('path', '') - } - except yaml.YAMLError as e: - print(f"YAML parsing error in {file_path}: {e}") - return None - - except Exception as e: - print(f"Error reading {file_path}: {e}") - return None - -def find_md_files_to_json(root_dir, output_file): - """Recursively find all markdown files, extract information, and save as JSON.""" - root_path = Path(root_dir) - - if not root_path.exists(): - print(f"Directory {root_dir} does not exist") - return - - # Find all .md files recursively - md_files = list(root_path.rglob('*.md')) - result = [] - - for md_file in sorted(md_files): - # Extract frontmatter data - frontmatter_data = extract_frontmatter_data(md_file) - - if not frontmatter_data: - print(f"Warning: No valid frontmatter in {md_file}") - continue - - # Add file path information - # frontmatter_data['filepath'] = str(md_file.relative_to(root_path)) - - # Add to result - result.append(frontmatter_data) - - # Save the result as JSON - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(result, f, indent=2) - - print(f"Processed {len(result)} markdown files") - print(f"Results saved to {output_file}") - return result - -def main(): - # Get the directory where this script is located - script_dir = Path(__file__).parent - tldr_dir = Path(os.path.dirname(os.path.abspath(__file__))).parent / "markdown_pages" / "tldr" - - if not tldr_dir.exists(): - print(f"Directory not found: {tldr_dir}") - tldr_dir = input("Please enter the path to the tldr directory: ") - tldr_dir = Path(tldr_dir) - - output_file = script_dir / "tldr_pages.json" - - print("Finding markdown files in:", tldr_dir) - print("Output will be saved to:", output_file) - print("=" * 80) - - find_md_files_to_json(tldr_dir, output_file) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/frontend/src/pages/tldr/index.astro b/frontend/src/pages/tldr/index.astro index 30200bd9f7..a5e889808d 100644 --- a/frontend/src/pages/tldr/index.astro +++ b/frontend/src/pages/tldr/index.astro @@ -1,67 +1,79 @@ --- +import { getAllTldrClusters } from '../../../db/tldrs/tldr-utils'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getAllTldrPlatforms } from '../../lib/tldr-utils'; import { formatNumber } from '../../lib/utils'; import Tldr from './_Tldr.astro'; -// Get all platforms with their data -const allPlatforms = await getAllTldrPlatforms(); +// Get page 1 of platforms +const start = Date.now(); +const allClusters = await getAllTldrClusters(); +// console.log( +// `[TLDR_PAGE] getAllTldrClusters() finished in ${Date.now() - start}ms` +// ); -// Calculate total commands across all platforms -const totalCommands = allPlatforms.reduce((total: number, platform: any) => total + platform.count, 0); +if (!allClusters) { + return Astro.redirect('/404'); +} -// Pagination logic for page 1 const itemsPerPage = 30; +const totalPlatforms = allClusters.length; +const totalPages = Math.ceil(totalPlatforms / itemsPerPage); const currentPage = 1; -const totalPages = Math.ceil(allPlatforms.length / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const platforms = allPlatforms.slice(startIndex, endIndex); -const totalPlatforms = allPlatforms.length; + +const platforms = allClusters.slice(0, itemsPerPage).map((c) => ({ + name: c.name, + count: c.count, + url: `/freedevtools/tldr/${c.name}/`, + commands: c.preview_commands, +})); + +const totalCommands = allClusters.reduce((acc, c) => acc + c.count, 0); // Breadcrumb data const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'TLDR' } + { label: 'TLDR' }, ]; // SEO data -const seoTitle = currentPage === 1 - ? "TLDR - Command Line Documentation | Online Free DevTools by Hexmos" - : `TLDR - Page ${currentPage} | Online Free DevTools by Hexmos`; +const seoTitle = + currentPage === 1 + ? 'TLDR - Command Line Documentation | Online Free DevTools by Hexmos' + : `TLDR - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = currentPage === 1 - ? `Comprehensive documentation for ${formatNumber(totalCommands)}+ command-line tools across different platforms. Learn commands quickly with practical examples.` - : `Browse page ${currentPage} of our TLDR command documentation. Learn command-line tools across different platforms.`; +const seoDescription = + currentPage === 1 + ? `Comprehensive documentation for ${formatNumber(totalCommands)}+ command-line tools across different platforms. Learn commands quickly with practical examples.` + : `Browse page ${currentPage} of our TLDR command documentation. Learn command-line tools across different platforms.`; -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/tldr/" - : `https://hexmos.com/freedevtools/tldr/${currentPage}/`; +const canonical = + currentPage === 1 + ? 'https://hexmos.com/freedevtools/tldr/' + : `https://hexmos.com/freedevtools/tldr/${currentPage}/`; // Enhanced keywords for main page const mainKeywords = [ - "tldr", - "command line", - "cli documentation", - "terminal commands", - "bash commands", - "linux commands", - "unix commands", - "command reference", - "cli documentation", - "terminal reference" + 'tldr', + 'command line', + 'cli documentation', + 'terminal commands', + 'bash commands', + 'linux commands', + 'unix commands', + 'command reference', + 'cli documentation', + 'terminal reference', ]; - --- - - - \ No newline at end of file + diff --git a/frontend/src/pages/tldr/sitemap-[page].xml.ts b/frontend/src/pages/tldr/sitemap-[page].xml.ts new file mode 100644 index 0000000000..56feca99e5 --- /dev/null +++ b/frontend/src/pages/tldr/sitemap-[page].xml.ts @@ -0,0 +1,60 @@ +import type { APIRoute } from "astro"; +import { getTldrSitemapUrls } from "db/tldrs/tldr-utils"; + +export const GET: APIRoute = async ({ params, site }) => { + const { page } = params; + + if (!page || !/^\d+$/.test(page)) { + return new Response(null, { status: 404 }); + } + + const pageNum = parseInt(page, 10); + const chunkSize = 5000; + const offset = (pageNum - 1) * chunkSize; + + const urls = await getTldrSitemapUrls(chunkSize, offset); + + if (!urls || urls.length === 0) { + return new Response(null, { status: 404 }); + } + + const now = new Date().toISOString(); + + // Use site from .env file (SITE variable) or astro.config.mjs + const envSite = process.env.SITE; + const siteStr = site?.toString() || ''; + const siteUrl = envSite || siteStr || 'http://localhost:4321/freedevtools'; + + const urlEntries = urls.map(url => { + // DB URL: /freedevtools/tldr/common/tar/ + // Site URL: http://localhost:4321/freedevtools + // Result should be: http://localhost:4321/freedevtools/tldr/common/tar/ + + // Strip /freedevtools from the start of DB URL if present + const relativeUrl = url.startsWith('/freedevtools') ? url.substring(13) : url; + const fullUrl = `${siteUrl}${relativeUrl}`; + + // Determine priority based on URL depth/type + let priority = '0.8'; + if (relativeUrl === '/tldr/') { + priority = '0.9'; // Landing page + } else if (relativeUrl.split('/').filter(Boolean).length === 2) { + priority = '0.8'; // Cluster page (e.g. /tldr/common/) + } else { + priority = '0.8'; // Command page + } + + return ` \n ${fullUrl}\n ${now}\n daily\n ${priority}\n `; + }); + + const xml = `\n\n\n${urlEntries.join( + "\n" + )}\n`; + + return new Response(xml, { + headers: { + "Content-Type": "application/xml", + "Cache-Control": "public, max-age=3600", + }, + }); +}; diff --git a/frontend/src/pages/tldr/sitemap.xml.ts b/frontend/src/pages/tldr/sitemap.xml.ts index 2e0e90b9a1..4d9098974c 100644 --- a/frontend/src/pages/tldr/sitemap.xml.ts +++ b/frontend/src/pages/tldr/sitemap.xml.ts @@ -1,86 +1,37 @@ -import type { APIRoute } from 'astro'; -import type { CollectionEntry } from 'astro:content'; -import { getCollection } from 'astro:content'; -import { - generateTldrPlatformStaticPaths, - generateTldrStaticPaths, -} from '../../lib/tldr-utils'; - -async function getCommandsByPlatform() { - const entries: CollectionEntry<'tldr'>[] = await getCollection('tldr'); - const byPlatform: Record = {}; - for (const entry of entries) { - const parts = entry.id.split('/'); - const platform = parts[parts.length - 2]; - const fileName = parts[parts.length - 1]; - const name = fileName.replace(/\.md$/i, ''); - if (!byPlatform[platform]) byPlatform[platform] = []; - byPlatform[platform].push({ - url: entry.data.path || `/freedevtools/tldr/${platform}/${name}`, - }); - } - return byPlatform; -} +import type { APIRoute } from "astro"; +import { getTldrSitemapCount } from "db/tldrs/tldr-utils"; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); - const byPlatform = await getCommandsByPlatform(); - const paginationPaths = await generateTldrStaticPaths(); - const platformPaginationPaths = await generateTldrPlatformStaticPaths(); - - if (!site) { - throw new Error('Site is not defined'); - } - - const urls: string[] = []; - // Category landing - urls.push( - ` \n ${site}/tldr/\n ${now}\n daily\n 0.9\n ` - ); - // Platform pages - for (const platform of Object.keys(byPlatform)) { - urls.push( - ` \n ${site}/tldr/${platform}/\n ${now}\n daily\n 0.8\n ` - ); - } - - // Main TLDR pagination pages (tldr/2/, tldr/3/, etc.) - for (const path of paginationPaths) { - const page = path.params.page; - urls.push( - ` \n ${site}/tldr/${page}/\n ${now}\n daily\n 0.8\n ` - ); - } - - // Platform pagination pages (tldr/linux/2/, tldr/windows/2/, etc.) - for (const path of platformPaginationPaths) { - const { platform, page } = path.params; - urls.push( - ` \n ${site}/tldr/${platform}/${page}/\n ${now}\n daily\n 0.8\n ` + + // Use site from .env file (SITE variable) or astro.config.mjs + const envSite = process.env.SITE; + const siteStr = site?.toString() || ''; + const siteUrl = envSite || siteStr || 'http://localhost:4321/freedevtools'; + + const totalUrls = await getTldrSitemapCount(); + const chunkSize = 5000; + const totalChunks = Math.ceil(totalUrls / chunkSize); + + const sitemaps: string[] = []; + + for (let i = 1; i <= totalChunks; i++) { + sitemaps.push( + ` + ${siteUrl}/tldr/sitemap-${i}.xml + ${now} + ` ); } - // Individual command pages - for (const [platform, commands] of Object.entries(byPlatform)) { - for (const cmd of commands) { - // Remove /freedevtools prefix and ensure proper URL construction - const cleanUrl = cmd.url.replace('/freedevtools', ''); - // Convert site URL to string and ensure proper URL construction - const siteStr = site.toString(); - const baseUrl = siteStr.endsWith('/') ? siteStr.slice(0, -1) : siteStr; - // Don't add extra slash - cleanUrl already has the correct path - const finalUrl = cleanUrl.startsWith('/') ? cleanUrl : `/${cleanUrl}`; - urls.push( - ` \n ${baseUrl}${finalUrl}\n ${now}\n daily\n 0.8\n ` - ); - } - } - const xml = `\n\n\n${urls.join('\n')}\n`; + const xml = `\n\n\n${sitemaps.join( + "\n" + )}\n`; return new Response(xml, { headers: { - 'Content-Type': 'application/xml', - 'Cache-Control': 'public, max-age=3600', + "Content-Type": "application/xml", + "Cache-Control": "public, max-age=3600", }, }); -}; +}; \ No newline at end of file diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 9289dcf4f7..244f5951e9 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -142,7 +142,8 @@ /* Global responsive text sizing for all p elements */ p, li { - @apply text-xs md:text-sm lg:text-base text-card-foreground leading-relaxed; + @apply text-xs md:text-sm lg:text-base leading-relaxed; + color: hsl(var(--card-foreground)); } } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.cjs similarity index 97% rename from frontend/tailwind.config.js rename to frontend/tailwind.config.cjs index 4d757fa966..9972f6086e 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.cjs @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ -import typography from '@tailwindcss/typography'; +const typography = require('@tailwindcss/typography'); module.exports = { mode: 'jit', @@ -75,3 +75,4 @@ module.exports = { }, }, }; + diff --git a/frontend/test-dockerfile-parser.js b/frontend/test-dockerfile-parser.js deleted file mode 100644 index 3bae517e00..0000000000 --- a/frontend/test-dockerfile-parser.js +++ /dev/null @@ -1,53 +0,0 @@ -// Quick test to verify the Dockerfile parser integration -const testDockerfiles = [ - { - name: "Valid Dockerfile", - content: `FROM alpine:latest -WORKDIR /app -COPY . . -RUN echo "hello world" -EXPOSE 8080 -CMD ["echo", "hello"]`, - }, - { - name: "Invalid - blahFROM", - content: `blahFROM alpine:latest -WORKDIR /app -COPY . . -RUN echo "hello world" -EXPOSE 8080 -CMD ["echo", "hello"]`, - }, - { - name: "Invalid - missing space", - content: `FROMalpine:latest -WORKDIR /app -COPY . . -RUN echo "hello world" -EXPOSE 8080 -CMD ["echo", "hello"]`, - }, - { - name: "Invalid - unknown instruction", - content: `FROM alpine:latest -WORKDIR /app -COPY . . -UNKNOWN_INSTRUCTION some parameters -EXPOSE 8080 -CMD ["echo", "hello"]`, - }, -]; - -// We can't directly import the React component, but we can test the core logic -console.log("Test Dockerfiles:"); -testDockerfiles.forEach((test, index) => { - console.log(`\n${index + 1}. ${test.name}:`); - console.log("Content:"); - console.log(test.content); - console.log("---"); -}); - -console.log("\nTo test these in the browser:"); -console.log("1. Go to http://localhost:4321/freedevtools/dockerfile-linter/"); -console.log("2. Paste each test case into the input"); -console.log("3. Check that syntax errors are properly detected"); diff --git a/frontend/test/ssr/tldr/tldr-utils.test.ts b/frontend/test/ssr/tldr/tldr-utils.test.ts new file mode 100644 index 0000000000..1c033f7bb2 --- /dev/null +++ b/frontend/test/ssr/tldr/tldr-utils.test.ts @@ -0,0 +1,110 @@ + +import { afterAll, describe, expect, it } from 'bun:test'; +import { + getAllTldrClusters, + getTldrCluster, + getTldrCommandsByClusterPaginated, + getTldrOverview, + getTldrPage, + getTldrSitemapCount, + getTldrSitemapUrls, +} from '../../../db/tldrs/tldr-utils'; +import { cleanupWorkers } from '../../../db/tldrs/tldr-worker-pool'; + +// Helper to measure execution time +async function measureTime(fn: () => Promise): Promise<{ result: T; duration: number }> { + const start = performance.now(); + const result = await fn(); + const end = performance.now(); + return { result, duration: end - start }; +} + +describe('TLDR SSR Utils', () => { + // Ensure workers are cleaned up after tests + afterAll(async () => { + await cleanupWorkers(); + }); + + it('getTldrOverview should return total count > 0', async () => { + const { result, duration } = await measureTime(() => getTldrOverview()); + console.log(`getTldrOverview took ${duration.toFixed(2)}ms`); + expect(result).toBeGreaterThan(0); + expect(typeof result).toBe('number'); + }); + + it('getAllTldrClusters should return list of clusters', async () => { + const { result, duration } = await measureTime(() => getAllTldrClusters()); + console.log(`getAllTldrClusters took ${duration.toFixed(2)}ms`); + expect(result.length).toBeGreaterThan(0); + + // Check for known clusters + const common = result.find(c => c.name === 'common'); + const linux = result.find(c => c.name === 'linux'); + expect(common).toBeDefined(); + expect(linux).toBeDefined(); + expect(common?.count).toBeGreaterThan(0); + }); + + it('getTldrCluster should return correct metadata for "common"', async () => { + const { result, duration } = await measureTime(() => getTldrCluster('common')); + console.log(`getTldrCluster('common') took ${duration.toFixed(2)}ms`); + expect(result).toBeDefined(); + expect(result?.name).toBe('common'); + expect(result?.count).toBeGreaterThan(0); + expect(result?.preview_commands.length).toBeGreaterThan(0); + }); + + it('getTldrCluster should return null for non-existent cluster', async () => { + const { result, duration } = await measureTime(() => getTldrCluster('non-existent-cluster-123')); + console.log(`getTldrCluster('non-existent') took ${duration.toFixed(2)}ms`); + expect(result).toBeNull(); + }); + + it('getTldrCommandsByClusterPaginated should return commands for "common"', async () => { + const { result, duration } = await measureTime(() => getTldrCommandsByClusterPaginated('common', 1, 10)); + console.log(`getTldrCommandsByClusterPaginated('common', 1, 10) took ${duration.toFixed(2)}ms`); + expect(result.length).toBeGreaterThan(0); + expect(result.length).toBeLessThanOrEqual(10); + + // Check structure + const cmd = result[0]; + expect(cmd.name).toBeDefined(); + expect(cmd.url).toBeDefined(); + expect(cmd.description).toBeDefined(); + }); + + it('getTldrCommandsByClusterPaginated should return empty array for out of range page', async () => { + const { result, duration } = await measureTime(() => getTldrCommandsByClusterPaginated('common', 9999, 10)); + console.log(`getTldrCommandsByClusterPaginated('common', 9999, 10) took ${duration.toFixed(2)}ms`); + expect(result).toEqual([]); + }); + + it('getTldrPage should return content for "common/tar"', async () => { + const { result, duration } = await measureTime(() => getTldrPage('common', 'tar')); + console.log(`getTldrPage('common', 'tar') took ${duration.toFixed(2)}ms`); + expect(result).toBeDefined(); + expect(result?.html_content).toContain('tar'); + expect(result?.title).toBeDefined(); + expect(result?.description).toBeDefined(); + expect(result?.metadata).toBeDefined(); + }); + + it('getTldrPage should return null for non-existent page', async () => { + const { result, duration } = await measureTime(() => getTldrPage('common', 'non-existent-command-123')); + console.log(`getTldrPage('common', 'non-existent') took ${duration.toFixed(2)}ms`); + expect(result).toBeNull(); + }); + + it('getTldrSitemapCount should return total count', async () => { + const { result, duration } = await measureTime(() => getTldrSitemapCount()); + console.log(`getTldrSitemapCount took ${duration.toFixed(2)}ms`); + expect(result).toBeGreaterThan(0); + }); + + it('getTldrSitemapUrls should return URLs', async () => { + const { result, duration } = await measureTime(() => getTldrSitemapUrls(10, 0)); + console.log(`getTldrSitemapUrls(10, 0) took ${duration.toFixed(2)}ms`); + expect(result.length).toBe(10); + expect(result[0]).toBe('/freedevtools/tldr/'); // Root should be first + }); +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts deleted file mode 100644 index c76552f12e..0000000000 --- a/frontend/vitest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; - -export default defineConfig({ - plugins: [react()], - test: { - globals: true, - environment: "jsdom", - setupFiles: ["./src/test/setup.ts"], - css: true, - }, - resolve: { - alias: { - "@": "/src", - }, - }, -});