feat: add build-time OG image generation with OpenAI
- Create generate-og-images.ts script for build-time image generation - Generate 13 static page OG images using gpt-image-1-mini - Update API route to serve pre-generated images from public/og/ - Add 30-day cache headers and fallback to default image - Images regenerate if older than 30 days Run with: pnpm --filter=@tpmjs/web generate-og 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
|
@ -10,7 +10,8 @@
|
|||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf .next .turbo",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"generate-og": "tsx scripts/generate-og-images.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "3.0.1",
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
"eslint-config-next": "^16.0.4",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^2.1.9"
|
||||
}
|
||||
|
|
|
|||
0
apps/web/public/og/.gitkeep
Normal file
BIN
apps/web/public/og/changelog.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
apps/web/public/og/docs.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
apps/web/public/og/faq.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
apps/web/public/og/home.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
apps/web/public/og/how-it-works.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
apps/web/public/og/playground.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
apps/web/public/og/privacy.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
apps/web/public/og/publish.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
apps/web/public/og/sdk.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
apps/web/public/og/spec.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
apps/web/public/og/stats.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
apps/web/public/og/terms.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
apps/web/public/og/tool-search.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
316
apps/web/scripts/generate-og-images.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Generate OG images for all pages
|
||||
* Run with: npx tsx scripts/generate-og-images.ts
|
||||
*/
|
||||
|
||||
import { mkdir, stat, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import OpenAI from 'openai';
|
||||
import { buildOGPrompt } from '../src/lib/og/prompt-builder';
|
||||
import type { PageContent } from '../src/lib/og/types';
|
||||
|
||||
const OUTPUT_DIR = path.join(process.cwd(), 'public', 'og');
|
||||
const MAX_AGE_DAYS = 30;
|
||||
|
||||
// Static pages to generate
|
||||
const STATIC_PAGES: Array<{ slug: string; content: PageContent }> = [
|
||||
{
|
||||
slug: 'home',
|
||||
content: {
|
||||
pageType: 'home',
|
||||
title: 'TPMJS',
|
||||
description: 'Tool Package Manager for AI Agents',
|
||||
keywords: ['AI', 'tools', 'npm', 'registry'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'docs',
|
||||
content: {
|
||||
pageType: 'docs',
|
||||
title: 'Documentation',
|
||||
description: 'Complete guide to using TPMJS',
|
||||
keywords: ['documentation', 'guide', 'tutorial'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'stats',
|
||||
content: {
|
||||
pageType: 'stats',
|
||||
title: 'Registry Statistics',
|
||||
description: 'Real-time metrics for TPMJS registry',
|
||||
keywords: ['statistics', 'metrics', 'analytics'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'publish',
|
||||
content: {
|
||||
pageType: 'publish',
|
||||
title: 'Publish Your Tool',
|
||||
description: 'Share your AI tool with the community',
|
||||
keywords: ['publish', 'create', 'npm'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'playground',
|
||||
content: {
|
||||
pageType: 'playground',
|
||||
title: 'Playground',
|
||||
description: 'Test and experiment with AI tools',
|
||||
keywords: ['playground', 'test', 'experiment'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'tool-search',
|
||||
content: {
|
||||
pageType: 'search',
|
||||
title: 'Tool Search',
|
||||
description: 'Search and discover AI tools',
|
||||
keywords: ['search', 'browse', 'discover'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'faq',
|
||||
content: {
|
||||
pageType: 'faq',
|
||||
title: 'FAQ',
|
||||
description: 'Frequently asked questions about TPMJS',
|
||||
keywords: ['faq', 'questions', 'help'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'sdk',
|
||||
content: {
|
||||
pageType: 'sdk',
|
||||
title: 'SDK',
|
||||
description: 'TPMJS SDK for integrating tools',
|
||||
keywords: ['sdk', 'integration', 'api'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'spec',
|
||||
content: {
|
||||
pageType: 'spec',
|
||||
title: 'Specification',
|
||||
description: 'TPMJS tool specification and schema',
|
||||
keywords: ['spec', 'specification', 'schema'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'changelog',
|
||||
content: {
|
||||
pageType: 'changelog',
|
||||
title: 'Changelog',
|
||||
description: 'TPMJS release history and updates',
|
||||
keywords: ['changelog', 'releases', 'updates'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'how-it-works',
|
||||
content: {
|
||||
pageType: 'how-it-works',
|
||||
title: 'How It Works',
|
||||
description: 'Learn how TPMJS works under the hood',
|
||||
keywords: ['how', 'works', 'architecture'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'privacy',
|
||||
content: {
|
||||
pageType: 'privacy',
|
||||
title: 'Privacy Policy',
|
||||
description: 'TPMJS privacy policy',
|
||||
keywords: ['privacy', 'policy', 'data'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'terms',
|
||||
content: {
|
||||
pageType: 'terms',
|
||||
title: 'Terms of Service',
|
||||
description: 'TPMJS terms and conditions',
|
||||
keywords: ['terms', 'service', 'legal'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let openai: OpenAI | null = null;
|
||||
|
||||
function getOpenAIClient(): OpenAI {
|
||||
if (!openai) {
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
throw new Error('OPENAI_API_KEY environment variable is required');
|
||||
}
|
||||
openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
}
|
||||
return openai;
|
||||
}
|
||||
|
||||
async function generateImage(prompt: string): Promise<Buffer> {
|
||||
const client = getOpenAIClient();
|
||||
|
||||
const response = await client.images.generate({
|
||||
model: 'gpt-image-1-mini',
|
||||
prompt,
|
||||
n: 1,
|
||||
size: '1536x1024',
|
||||
quality: 'medium',
|
||||
});
|
||||
|
||||
if (!response.data || response.data.length === 0) {
|
||||
throw new Error('No data returned from OpenAI');
|
||||
}
|
||||
|
||||
const b64 = response.data[0]?.b64_json;
|
||||
if (!b64) {
|
||||
throw new Error('No base64 image data returned');
|
||||
}
|
||||
|
||||
return Buffer.from(b64, 'base64');
|
||||
}
|
||||
|
||||
async function shouldRegenerate(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const stats = await stat(filePath);
|
||||
const ageMs = Date.now() - stats.mtime.getTime();
|
||||
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
||||
return ageDays > MAX_AGE_DAYS;
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateStaticPages(): Promise<void> {
|
||||
console.log('\n📄 Generating static page images...\n');
|
||||
|
||||
for (const page of STATIC_PAGES) {
|
||||
const filePath = path.join(OUTPUT_DIR, `${page.slug}.png`);
|
||||
|
||||
if (!(await shouldRegenerate(filePath))) {
|
||||
console.log(` ⏭️ ${page.slug} - skipped (fresh)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` 🎨 ${page.slug} - generating...`);
|
||||
|
||||
try {
|
||||
const prompt = buildOGPrompt(page.content);
|
||||
const buffer = await generateImage(prompt);
|
||||
await writeFile(filePath, buffer);
|
||||
console.log(` ✅ ${page.slug} - saved`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ ${page.slug} - failed:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTools(): Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
packageName: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}>
|
||||
> {
|
||||
// Fetch tools from the API
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/tools?limit=100`);
|
||||
if (!response.ok) {
|
||||
console.warn('Failed to fetch tools from API, skipping tool images');
|
||||
return [];
|
||||
}
|
||||
|
||||
const { data } = await response.json();
|
||||
return data.tools || [];
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch tools:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function slugifyToolPath(packageName: string, toolName: string): string {
|
||||
// Convert @scope/package/toolName to scope-package-toolName
|
||||
return `${packageName}/${toolName}`
|
||||
.replace(/^@/, '')
|
||||
.replace(/\//g, '-')
|
||||
.replace(/[^a-zA-Z0-9-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
async function generateToolPages(): Promise<void> {
|
||||
console.log('\n🔧 Generating tool page images...\n');
|
||||
|
||||
const tools = await fetchTools();
|
||||
|
||||
if (tools.length === 0) {
|
||||
console.log(' ⚠️ No tools found, skipping tool images');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` Found ${tools.length} tools\n`);
|
||||
|
||||
// Create tools subdirectory
|
||||
const toolsDir = path.join(OUTPUT_DIR, 'tool');
|
||||
await mkdir(toolsDir, { recursive: true });
|
||||
|
||||
for (const tool of tools) {
|
||||
const slug = slugifyToolPath(tool.packageName, tool.name);
|
||||
const filePath = path.join(toolsDir, `${slug}.png`);
|
||||
|
||||
if (!(await shouldRegenerate(filePath))) {
|
||||
console.log(` ⏭️ ${tool.name} - skipped (fresh)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` 🎨 ${tool.name} - generating...`);
|
||||
|
||||
try {
|
||||
const content: PageContent = {
|
||||
pageType: 'tool',
|
||||
title: tool.name,
|
||||
description: tool.description || `AI tool from ${tool.packageName}`,
|
||||
keywords: [tool.category, 'AI', 'tool'],
|
||||
tool: {
|
||||
name: tool.name,
|
||||
packageName: tool.packageName,
|
||||
category: tool.category || 'other',
|
||||
description: tool.description || '',
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = buildOGPrompt(content);
|
||||
const buffer = await generateImage(prompt);
|
||||
await writeFile(filePath, buffer);
|
||||
console.log(` ✅ ${tool.name} - saved`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ ${tool.name} - failed:`, error);
|
||||
}
|
||||
|
||||
// Rate limit - wait 500ms between requests
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('🖼️ TPMJS OG Image Generator\n');
|
||||
console.log(`Output directory: ${OUTPUT_DIR}`);
|
||||
console.log(`Max age: ${MAX_AGE_DAYS} days`);
|
||||
|
||||
// Ensure output directory exists
|
||||
await mkdir(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
// Generate static pages
|
||||
await generateStaticPages();
|
||||
|
||||
// Generate tool pages
|
||||
await generateToolPages();
|
||||
|
||||
console.log('\n✨ Done!\n');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
|
@ -1,31 +1,59 @@
|
|||
/**
|
||||
* OG Image Generation API Route
|
||||
* OG Image API Route
|
||||
*
|
||||
* GET /api/og/[...path]
|
||||
*
|
||||
* Generates unique OpenGraph images for each page using OpenAI gpt-image-1-mini.
|
||||
* Images are cached in Vercel Blob storage for 30 days.
|
||||
* Serves pre-generated OG images from public/og/ directory.
|
||||
* Images are generated at build time using: pnpm --filter=@tpmjs/web generate-og
|
||||
*
|
||||
* Examples:
|
||||
* /api/og/home -> Homepage OG image
|
||||
* /api/og/docs -> Docs page OG image
|
||||
* /api/og/tool/@tpmjs/hello/helloWorld -> Tool-specific OG image
|
||||
* /api/og/home -> public/og/home.png
|
||||
* /api/og/docs -> public/og/docs.png
|
||||
* /api/og/tool/@tpmjs/hello/helloWorld -> public/og/tool/tpmjs-hello-helloworld.png
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
buildOGPrompt,
|
||||
cacheImage,
|
||||
extractPageContent,
|
||||
generateOGImage,
|
||||
getCachedImage,
|
||||
} from '~/lib/og';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60;
|
||||
|
||||
/**
|
||||
* Convert a path to the corresponding OG image filename
|
||||
* /home -> home.png
|
||||
* /tool/@tpmjs/hello/helloWorld -> tool/tpmjs-hello-helloworld.png
|
||||
*/
|
||||
function pathToFilename(pagePath: string): string {
|
||||
// Remove leading slash
|
||||
const cleanPath = pagePath.replace(/^\//, '');
|
||||
|
||||
// Handle tool pages specially
|
||||
if (cleanPath.startsWith('tool/')) {
|
||||
// Extract package and tool name from path like tool/@scope/package/toolName
|
||||
const toolPath = cleanPath.replace('tool/', '');
|
||||
const slug = toolPath
|
||||
.replace(/^@/, '')
|
||||
.replace(/\//g, '-')
|
||||
.replace(/[^a-zA-Z0-9-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.toLowerCase();
|
||||
return `tool/${slug}.png`;
|
||||
}
|
||||
|
||||
return `${cleanPath}.png`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve an image file with proper headers
|
||||
*/
|
||||
function serveImage(buffer: Buffer): NextResponse {
|
||||
return new NextResponse(new Uint8Array(buffer), {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=2592000, stale-while-revalidate=86400', // 30 days
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the static fallback OG image
|
||||
|
|
@ -34,15 +62,8 @@ async function serveFallbackImage(): Promise<NextResponse> {
|
|||
try {
|
||||
const fallbackPath = path.join(process.cwd(), 'public', 'og-image.png');
|
||||
const buffer = await readFile(fallbackPath);
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
||||
},
|
||||
});
|
||||
return serveImage(buffer);
|
||||
} catch {
|
||||
// If even the fallback fails, return a simple error
|
||||
return new NextResponse('Fallback image not found', { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -51,44 +72,28 @@ export async function GET(
|
|||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
): Promise<NextResponse> {
|
||||
const startTime = Date.now();
|
||||
const { path: pathSegments } = await params;
|
||||
const pagePath = `/${pathSegments.join('/')}`;
|
||||
|
||||
console.log(`[OG] Generating image for: ${pagePath}`);
|
||||
|
||||
try {
|
||||
// 1. Check cache first
|
||||
const cachedUrl = await getCachedImage(pagePath);
|
||||
if (cachedUrl) {
|
||||
console.log(`[OG] Cache hit for: ${pagePath} (${Date.now() - startTime}ms)`);
|
||||
// Redirect to the cached blob URL
|
||||
return NextResponse.redirect(cachedUrl, { status: 302 });
|
||||
// Convert path to filename
|
||||
const filename = pathToFilename(pagePath);
|
||||
const imagePath = path.join(process.cwd(), 'public', 'og', filename);
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await stat(imagePath);
|
||||
} catch {
|
||||
// File doesn't exist, serve fallback
|
||||
console.log(`[OG] Image not found: ${imagePath}, serving fallback`);
|
||||
return serveFallbackImage();
|
||||
}
|
||||
|
||||
console.log(`[OG] Cache miss for: ${pagePath}, generating...`);
|
||||
|
||||
// 2. Extract page content
|
||||
const content = await extractPageContent(pagePath);
|
||||
console.log(`[OG] Content extracted: ${content.pageType} - ${content.title}`);
|
||||
|
||||
// 3. Build prompt
|
||||
const prompt = buildOGPrompt(content);
|
||||
|
||||
// 4. Generate image with OpenAI
|
||||
const imageBuffer = await generateOGImage(prompt);
|
||||
console.log(`[OG] Image generated (${imageBuffer.length} bytes)`);
|
||||
|
||||
// 5. Cache the image in Vercel Blob
|
||||
const blobUrl = await cacheImage(pagePath, imageBuffer);
|
||||
console.log(`[OG] Cached to: ${blobUrl} (${Date.now() - startTime}ms)`);
|
||||
|
||||
// 6. Redirect to the blob URL
|
||||
return NextResponse.redirect(blobUrl, { status: 302 });
|
||||
// Read and serve the image
|
||||
const buffer = await readFile(imagePath);
|
||||
return serveImage(buffer);
|
||||
} catch (error) {
|
||||
console.error(`[OG] Generation failed for ${pagePath}:`, error);
|
||||
|
||||
// Fall back to static image
|
||||
console.error(`[OG] Error serving image for ${pagePath}:`, error);
|
||||
return serveFallbackImage();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ export async function generateOGImage(prompt: string): Promise<Buffer> {
|
|||
console.log('[OG] Calling OpenAI image generation...');
|
||||
|
||||
const response = await client.images.generate({
|
||||
model: 'gpt-image-1',
|
||||
model: 'gpt-image-1-mini',
|
||||
prompt,
|
||||
n: 1,
|
||||
size: '1536x1024', // Landscape format for OG images
|
||||
quality: 'high',
|
||||
quality: 'medium',
|
||||
});
|
||||
|
||||
console.log('[OG] OpenAI response received');
|
||||
|
|
|
|||
13
pnpm-lock.yaml
generated
|
|
@ -277,6 +277,9 @@ importers:
|
|||
tailwindcss:
|
||||
specifier: ^3.4.17
|
||||
version: 3.4.18(tsx@4.21.0)
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
|
@ -9543,7 +9546,7 @@ snapshots:
|
|||
'@next/eslint-plugin-next': 16.0.4
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7))
|
||||
|
|
@ -9586,7 +9589,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
|
|
@ -9626,13 +9629,13 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)):
|
||||
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -9686,7 +9689,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
|
|||