Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/__tests__/commands/multi-scrape-filenames.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Tests for where multi-URL scrape saves each page
*/

import * as fs from 'fs';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { handleMultiScrapeCommand } from '../../commands/scrape';
import { getClient } from '../../utils/client';

vi.mock('../../utils/client', () => ({
getClient: vi.fn(),
isKeylessMode: () => false,
keylessRequest: vi.fn(),
}));
vi.mock('../../utils/interact-session', () => ({
saveInteractSession: vi.fn(),
clearInteractSession: vi.fn(),
}));
vi.mock('fs', async () => ({
...(await vi.importActual<typeof import('fs')>('fs')),
existsSync: vi.fn(() => true),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
}));

describe('multi-URL scrape filenames', () => {
beforeEach(() => {
vi.mocked(getClient).mockReturnValue({
scrape: vi.fn(async (url: string) => ({ markdown: `page ${url}` })),
} as any);
vi.spyOn(process.stderr, 'write').mockReturnValue(true);
});

afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});

it('saves every URL to its own file when their names would collide', async () => {
const urls = [
'https://example.com/list?page=1',
'https://example.com/list?page=2',
'https://example.com/a/b',
'https://example.com/a-b',
];

await handleMultiScrapeCommand(urls, { url: urls[0] });

const written = new Map(
vi
.mocked(fs.writeFileSync)
.mock.calls.map(([file, content]) => [String(file), String(content)])
);
expect(written).toEqual(
new Map([
[
join('.firecrawl', 'example.com-list.md'),
'page https://example.com/list?page=1',
],
[
join('.firecrawl', 'example.com-list-2.md'),
'page https://example.com/list?page=2',
],
[
join('.firecrawl', 'example.com-a-b.md'),
'page https://example.com/a/b',
],
[
join('.firecrawl', 'example.com-a-b-2.md'),
'page https://example.com/a-b',
],
])
);
});

it('keeps the plain name for URLs that do not collide', async () => {
const urls = ['https://example.com/', 'https://example.com/docs/intro'];

await handleMultiScrapeCommand(urls, { url: urls[0] });

const files = vi
.mocked(fs.writeFileSync)
.mock.calls.map(([file]) => String(file))
.sort();
expect(files).toEqual([
join('.firecrawl', 'example.com-docs-intro.md'),
join('.firecrawl', 'example.com.md'),
]);
});
});
25 changes: 22 additions & 3 deletions src/commands/scrape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,24 @@ function urlToFilename(url: string): string {
}
}

/**
* Pick one filename per URL. URLs that differ only in their query string or
* in `/` versus `-` in the path map to the same name, so later ones get a
* numeric suffix instead of overwriting an earlier page from the same batch.
*/
function uniqueFilenames(urls: string[]): string[] {
const used = new Set<string>();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
return urls.map((url) => {
const base = urlToFilename(url);
let name = base;
for (let n = 2; used.has(name); n++) {
name = base.replace(/\.md$/, `-${n}.md`);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}
used.add(name);
return name;
});
}

/**
* Handle scrape for multiple URLs.
* Each result is saved as a separate file in .firecrawl/
Expand All @@ -277,7 +295,9 @@ export async function handleMultiScrapeCommand(

process.stderr.write(`Scraping ${total} URLs...\n`);

const promises = urls.map(async (url) => {
const filenames = uniqueFilenames(urls);

const promises = urls.map(async (url, index) => {
const scrapeOptions: ScrapeOptions = { ...options, url };
const result = await executeScrape(scrapeOptions);

Expand All @@ -291,8 +311,7 @@ export async function handleMultiScrapeCommand(
return;
}

const filename = urlToFilename(url);
const filepath = path.join(dir, filename);
const filepath = path.join(dir, filenames[index]);
const content = result.data?.markdown || JSON.stringify(result.data);
fs.writeFileSync(filepath, content, 'utf-8');

Expand Down