
Security News
Deno Goes the Way of the Dinosaur
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.
High-performance web crawler and scraper for TypeScript, powered by Bun and Playwright
High-performance web crawler and scraper for TypeScript, powered by Bun and Playwright.
bun:sqlite@e refs for AI consumptionwaitUntil: commit (fastest), domcontentloaded, load, networkidlepage.evaluate(), skipping HTML serializationbun add feedstock
bunx playwright install chromium
import { WebCrawler, CacheMode } from "feedstock";
const crawler = new WebCrawler();
const result = await crawler.crawl("https://example.com", {
cacheMode: CacheMode.Bypass,
});
console.log(result.markdown?.rawMarkdown);
console.log(result.links.internal);
console.log(result.media.images);
await crawler.close();
Recursively crawl entire sites with filters, rate limiting, and robots.txt compliance:
import {
WebCrawler, CacheMode,
FilterChain, DomainFilter, ContentTypeFilter, URLPatternFilter,
RateLimiter, RobotsParser,
CompositeScorer, KeywordRelevanceScorer, PathDepthScorer,
} from "feedstock";
const crawler = new WebCrawler();
const results = await crawler.deepCrawl(
"https://example.com",
{ cacheMode: CacheMode.Bypass },
{
maxDepth: 3,
maxPages: 100,
concurrency: 5,
filterChain: new FilterChain()
.add(new DomainFilter({ allowed: ["example.com"] }))
.add(new ContentTypeFilter())
.add(new URLPatternFilter({ exclude: [/\/admin/, /\/login/] })),
rateLimiter: new RateLimiter({ baseDelay: 500 }),
robotsParser: new RobotsParser(),
scorer: new CompositeScorer()
.add(new KeywordRelevanceScorer(["docs", "api"], 2.0))
.add(new PathDepthScorer()),
},
);
Stream results as they arrive for large crawls:
for await (const result of crawler.deepCrawlStream(startUrl, {}, config)) {
console.log(`Crawled: ${result.url}`);
}
const result = await crawler.crawl("https://example.com/products", {
extractionStrategy: {
type: "css",
params: {
name: "products",
baseSelector: ".product",
fields: [
{ name: "title", selector: "h2", type: "text" },
{ name: "price", selector: ".price", type: "text" },
{ name: "image", selector: "img", type: "attribute", attribute: "src" },
{ name: "tags", selector: ".tag", type: "list" },
],
},
},
});
const products = JSON.parse(result.extractedContent!)
.map((item) => JSON.parse(item.content));
import { TableExtractionStrategy } from "feedstock";
const strategy = new TableExtractionStrategy({ minRows: 2 });
const tables = await strategy.extract(url, html);
// [{ headers: ["Name", "Age"], rows: [["Alice", "30"], ...], caption: "Users" }]
import { RegexExtractionStrategy } from "feedstock";
const strategy = new RegexExtractionStrategy([
/\$(?<dollars>\d+)\.(?<cents>\d{2})/g,
]);
const prices = await strategy.extract(url, html);
// prices[0].metadata.groups = { dollars: "9", cents: "99" }
const crawler = new WebCrawler({
config: {
browserType: "chromium", // or "firefox", "webkit"
headless: true,
},
});
// Local (requires: bun add @lightpanda/browser)
const crawler = new WebCrawler({
config: {
backend: { kind: "lightpanda", mode: "local" },
},
});
// Cloud
const crawler = new WebCrawler({
config: {
backend: {
kind: "lightpanda",
mode: "cloud",
token: process.env.LIGHTPANDA_TOKEN!,
},
},
});
One flag — random user-agents, navigator overrides, and human simulation:
// Enable stealth at browser level
const crawler = new WebCrawler({
config: { stealth: true },
});
// Human simulation per-crawl
const result = await crawler.crawl(url, {
simulateUser: true, // random mouse movements + scrolling
});
// Auto-retry on blocks
import { withRetry, isBlocked } from "feedstock";
const { result, retries } = await withRetry(
() => crawler.crawl(url),
(r) => isBlocked(r.html, r.statusCode ?? 200),
{ maxRetries: 3, retryDelay: 2000 },
);
import { ProxyRotationStrategy } from "feedstock";
const rotation = new ProxyRotationStrategy([
{ server: "http://proxy1:8080" },
{ server: "http://proxy2:8080" },
{ server: "http://proxy3:8080" },
]);
const proxy = rotation.getProxy(); // round-robin, skips unhealthy
rotation.reportResult(proxy, true); // track health
import { SlidingWindowChunking, RegexChunking } from "feedstock";
// Split by paragraphs
new RegexChunking().chunk(text);
// Sliding window with overlap
new SlidingWindowChunking(500, 50).chunk(text);
import { PruningContentFilter, BM25ContentFilter } from "feedstock";
// Remove boilerplate
new PruningContentFilter({ minWords: 5 }).filter(content);
// Keep only relevant blocks
new BM25ContentFilter({ threshold: 0.1 }).filter(content, "TypeScript crawler");
// Block images/CSS/fonts for content-only crawl
const result = await crawler.crawl("https://example.com", {
blockResources: true,
navigationWaitUntil: "commit", // fastest — returns as soon as server responds
});
Extract data directly in the browser — skips HTML serialization round-trip:
import { extractInPage } from "feedstock";
crawler.setHook("beforeReturnHtml", async (page) => {
const data = await extractInPage(page);
// data.links, data.media, data.metadata — extracted inside browser context
});
import { URLSeeder } from "feedstock";
const seeder = new URLSeeder();
const { urls, sitemaps } = await seeder.seed("example.com");
// Discovers URLs from robots.txt -> sitemap.xml chain
import { CrawlerMonitor } from "feedstock";
const monitor = new CrawlerMonitor();
monitor.start();
// Track each page
monitor.recordPageComplete({
success: true,
fromCache: false,
responseTimeMs: 150,
bytesDownloaded: 45_000,
});
console.log(monitor.formatStats());
// Pages: 1 (1 ok, 0 failed, 0 cached)
// Time: 0.2s | 6.7 pages/s | avg 150ms/page
// Downloaded: 0.04 MB
Compact semantic page representation — 3-10x smaller than HTML:
const result = await crawler.crawl("https://example.com", {
snapshot: true,
});
console.log(result.snapshot);
// @e1 [heading] "Example Domain" [level=1]
// @e2 [link] "More information..." [-> https://www.iana.org/domains/example]
Inject custom behavior at key lifecycle points:
crawler.setHook("afterGoto", async (page) => {
// Dismiss cookie banners, expand sections, etc.
const banner = page.locator('[class*="cookie"]');
if (await banner.isVisible()) await banner.locator("button").first().click();
});
Available hooks: onPageCreated, beforeGoto, afterGoto, onExecutionStarted, beforeReturnHtml.
Find all clickable elements including those without ARIA roles:
import { detectInteractiveElements } from "feedstock";
crawler.setHook("beforeReturnHtml", async (page) => {
const elements = await detectInteractiveElements(page);
console.log(`Found ${elements.length} interactive elements`);
// Each has: tag, text, href, role, type, selector
});
Persist cookies/localStorage between sessions:
import { saveStorageState, loadStorageState } from "feedstock";
// Save after login
await saveStorageState(page.context());
// Load in next session
const state = loadStorageState();
const html = "<html><body><h1>Hello</h1><p>World</p></body></html>";
const result = await crawler.processHtml(html, { snapshot: true });
console.log(result.markdown?.rawMarkdown);
console.log(result.snapshot);
Full documentation at usefeedstock.com.
bun install
bun test # 277 tests
bun test tests/unit # unit tests only
bun run typecheck # tsc --noEmit
bun run lint # biome check
bun run check # lint + typecheck
bun run dogfood.ts # 148 checks against real sites
Apache-2.0
FAQs
High-performance web crawler and scraper for TypeScript, powered by Bun and Playwright
The npm package feedstock receives a total of 6 weekly downloads. As such, feedstock popularity was classified as not popular.
We found that feedstock demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.

Security News
Anthropic’s free OSS Scanner uses Mythos and other Claude models to find open source vulnerabilities and send maintainers reports with proposed fixes.

Security News
On Front Lines’ Early Adopters: CISO podcast, Andrew Becherer shares lessons from Datadog, weighs the long-term costs of AI-built tools, and explains how founders can actually gain a CISO’s trust.