Repository: mongodb/leafygreen-ui
Pull request #2849: Ingest with recursive crawler
Author: login: TheSonOfThomp
Created: 2025-05-08T18:01:06Z | Merged: 2025-05-09T14:46:31Z

Update ingest to use `recursiveCrawlFromBaseURL` from `@lg-tools/crawler`

Update `recursiveCrawlFromBaseURL` to accept callback instead of baking in the document processing


Diff:
diff --git a/apps/chatbot-server/ingest/ingest.config.ts b/apps/chatbot-server/ingest/ingest.config.ts
index e6a8a20599..90ff7dcd24 100644
--- a/apps/chatbot-server/ingest/ingest.config.ts
+++ b/apps/chatbot-server/ingest/ingest.config.ts
@@ -5,10 +5,11 @@ import {
 import { AzureOpenAI } from 'mongodb-rag-core/openai';
 import { Config, makeIngestMetaStore } from 'mongodb-rag-ingest';
 
-import { leafygreenGithubSourceConstructor } from './sources/github-leafygreen-ui';
-import { mongoDbChatbotFrameworkDocsDataSourceConstructor } from './sources/github-mdb-chatbot-framework';
-import { createAzureEmbedderConstructor } from './utils/createAzureEmbedderConstructor';
-import { loadEnvVars } from './utils/loadEnv';
+import { leafygreenGithubSourceConstructor } from './sources/github-leafygreen-ui.js';
+import { mongoDbChatbotFrameworkDocsDataSourceConstructor } from './sources/github-mdb-chatbot-framework.js';
+import { createAzureEmbedderConstructor } from './utils/createAzureEmbedderConstructor.js';
+import { loadEnvVars } from './utils/loadEnv.js';
+import { webSourceConstructor } from './utils/webSourceConstructor.js';
 
 // Load project environment variables
 const {
@@ -49,6 +50,15 @@ export default {
   // Add data sources here
   dataSources: async () => {
     return Promise.all([
+      ...[
+        'https://mongodb.design',
+        'https://react.dev/reference/react',
+        'https://developer.mozilla.org/en-US/docs/Web',
+        'https://css-tricks.com/category/articles',
+        'https://www.nngroup.com/articles',
+        'https://www.w3.org/WAI/standards-guidelines/wcag',
+        'https://atomicdesign.bradfrost.com/table-of-contents',
+      ].map(source => webSourceConstructor(source, {})),
       mongoDbChatbotFrameworkDocsDataSourceConstructor(),
       leafygreenGithubSourceConstructor(),
     ]);
diff --git a/apps/chatbot-server/ingest/tsconfig.json b/apps/chatbot-server/ingest/tsconfig.json
index 5615956040..ef5d351f5f 100644
--- a/apps/chatbot-server/ingest/tsconfig.json
+++ b/apps/chatbot-server/ingest/tsconfig.json
@@ -16,4 +16,9 @@
     "./ingest.config.ts"
   ],
   "include": ["./**/*.ts"],
+  "references": [
+    {
+      "path": "../../../tools/crawler"
+    },
+  ]
 }
diff --git a/apps/chatbot-server/ingest/utils/webSourceConstructor.ts b/apps/chatbot-server/ingest/utils/webSourceConstructor.ts
new file mode 100644
index 0000000000..eeeaac8b80
--- /dev/null
+++ b/apps/chatbot-server/ingest/utils/webSourceConstructor.ts
@@ -0,0 +1,56 @@
+/* eslint-disable no-console */
+import { recursiveCrawlFromBaseURL } from '@lg-tools/crawler';
+import { Page } from 'mongodb-rag-core';
+import { type DataSource } from 'mongodb-rag-core/dataSources';
+
+export interface WebSourceConstructorOptions {
+  maxDepth?: number;
+  verbose?: boolean;
+}
+
+/**
+ * Returns a constructor function
+ * that creates a web source
+ */
+export async function webSourceConstructor(
+  source: string,
+  options?: WebSourceConstructorOptions,
+): Promise<DataSource> {
+  const { maxDepth = 3, verbose = false } = {
+    maxDepth: 3,
+    verbose: false,
+    ...options,
+  };
+  return {
+    name: source,
+    fetchPages: async () => {
+      verbose && console.log(`🐶 Fetching source ${source}`);
+      const pages: Array<Page> = [];
+
+      await recursiveCrawlFromBaseURL(
+        ({ document, title, href }) => {
+          verbose && console.log(`🪲 Crawled page ${title} - ${href}`);
+          pages.push({
+            url: href,
+            title,
+            body: document.pageContent,
+            format: 'txt',
+            sourceName: source,
+            metadata: {
+              id: document.id,
+              ...document.metadata,
+            },
+          });
+        },
+        {
+          baseUrl: source,
+          maxDepth,
+          verbose,
+          enableRecursion: true,
+        },
+      );
+
+      return pages;
+    },
+  };
+}
diff --git a/apps/chatbot-server/package.json b/apps/chatbot-server/package.json
index f1030d8337..a197326f29 100644
--- a/apps/chatbot-server/package.json
+++ b/apps/chatbot-server/package.json
@@ -2,6 +2,7 @@
   "name": "@leafygreen-ui/chatbot-server",
   "version": "0.0.1",
   "description": "",
+  "type": "module",
   "main": "index.js",
   "publishConfig": {
     "access": "restricted"
@@ -15,6 +16,7 @@
   "author": "",
   "license": "Apache-2.0",
   "dependencies": {
+    "@lg-tools/crawler": "workspace:^",
     "@emotion/css": "^11.13.5",
     "dotenv": "^16.5.0",
     "jsdom": "^26.1.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ece9752054..687ffb2fbc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -116,6 +116,9 @@ importers:
       '@emotion/css':
         specifier: ^11.13.5
         version: 11.13.5
+      '@lg-tools/crawler':
+        specifier: workspace:^
+        version: link:../../tools/crawler
       dotenv:
         specifier: ^16.5.0
         version: 16.5.0
@@ -3115,7 +3118,7 @@ importers:
       langchain:
         specifier: ^0.3.24
         version: 0.3.24(@langchain/anthropic@0.3.20(@langchain/core@0.3.54(openai@4.97.0(ws@8.18.2)(zod@3.24.4))))(@langchain/core@0.3.54(openai@4.97.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0)(cheerio@1.0.0)(openai@4.97.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2)
-      lodash:
+      lodash-es:
         specifier: ^4.17.21
         version: 4.17.21
       mongodb:
@@ -3134,6 +3137,9 @@ importers:
       '@lg-tools/meta':
         specifier: workspace:^
         version: link:../meta
+      '@types/lodash-es':
+        specifier: ^4.17.12
+        version: 4.17.12
       tsx:
         specifier: ^4.19.4
         version: 4.19.4
@@ -6685,6 +6691,9 @@ packages:
   '@types/jsonfile@6.1.1':
     resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==}
 
+  '@types/lodash-es@4.17.12':
+    resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
+
   '@types/lodash.debounce@4.0.9':
     resolution: {integrity: sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==}
 
@@ -9817,6 +9826,9 @@ packages:
     resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
 
+  lodash-es@4.17.21:
+    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+
   lodash.debounce@4.0.8:
     resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
 
@@ -15697,6 +15709,10 @@ snapshots:
     dependencies:
       '@types/node': 20.4.8
 
+  '@types/lodash-es@4.17.12':
+    dependencies:
+      '@types/lodash': 4.17.10
+
   '@types/lodash.debounce@4.0.9':
     dependencies:
       '@types/lodash': 4.17.10
@@ -19668,6 +19684,8 @@ snapshots:
     dependencies:
       p-locate: 6.0.0
 
+  lodash-es@4.17.21: {}
+
   lodash.debounce@4.0.8: {}
 
   lodash.includes@4.3.0: {}
diff --git a/tools/crawler/package.json b/tools/crawler/package.json
index 42fa0d7232..b1a932deef 100644
--- a/tools/crawler/package.json
+++ b/tools/crawler/package.json
@@ -6,6 +6,11 @@
   "main": "./dist/index.js",
   "module": "./dist/esm/index.js",
   "types": "./dist/types/index.d.ts",
+  "exports": {
+    "types": "./dist/types/index.d.ts",
+    "require": "./dist/index.js",
+    "import": "./dist/esm/index.js"
+  },
   "bin": {
     "lg-crawler": "./bin/cli.js"
   },
@@ -42,12 +47,13 @@
     "commander": "^13.1.0",
     "dotenv": "^16.5.0",
     "langchain": "^0.3.24",
-    "lodash": "^4.17.21",
+    "lodash-es": "^4.17.21",
     "mongodb": "^6.16.0",
     "openai": "^4.97.0",
     "ora": "^8.2.0"
   },
   "devDependencies": {
+    "@types/lodash-es": "^4.17.12",
     "@lg-tools/build": "workspace:^",
     "@lg-tools/meta": "workspace:^",
     "tsx": "^4.19.4"
diff --git a/tools/crawler/src/constants.ts b/tools/crawler/src/constants.ts
index 5b57e34488..28e14198f1 100644
--- a/tools/crawler/src/constants.ts
+++ b/tools/crawler/src/constants.ts
@@ -42,13 +42,10 @@ export const SOURCES = [
     url: 'https://atomicdesign.bradfrost.com/table-of-contents',
     collection: 'atomic-design',
   },
-] as const;
+];
 
 /**
  * Allow the crawler to follow links to these domains
  * (with restricted depth)
  */
-export const allowedDomains = [
-  'https://www.mongodb.com',
-  'https://github.com',
-] as const;
+export const allowedDomains = ['https://www.mongodb.com', 'https://github.com'];
diff --git a/tools/crawler/src/crawler.ts b/tools/crawler/src/crawler.ts
index 9f42daef30..bc321c2f0e 100644
--- a/tools/crawler/src/crawler.ts
+++ b/tools/crawler/src/crawler.ts
@@ -3,6 +3,7 @@ import chalk from 'chalk';
 
 import { connectToMongoDB } from './utils/connectToMongoDB';
 import { createCollectionNameFromURL } from './utils/createCollectionNameFromURL';
+import { processLangchainDocument } from './utils/processLangchainDocument';
 import { recursiveCrawlFromBaseURL } from './utils/recursiveCrawlFromBase';
 import { SOURCES } from './constants';
 import { CrawlerOptions } from './types';
@@ -36,14 +37,23 @@ export async function crawl(options: CrawlerOptions) {
               `Crawling URL ${url} with collection name ${matchedSource.collection}`,
             ),
           );
-        await recursiveCrawlFromBaseURL({
-          baseUrl: url,
-          collectionName: matchedSource.collection,
-          mongoClient,
-          maxDepth,
-          verbose,
-          dryRun,
-        });
+        await recursiveCrawlFromBaseURL(
+          ({ document, title, href }) => {
+            processLangchainDocument({
+              doc: document,
+              title,
+              href,
+              collectionName: matchedSource.collection,
+              mongoClient,
+              dryRun,
+            });
+          },
+          {
+            baseUrl: url,
+            maxDepth,
+            verbose,
+          },
+        );
       } else {
         const newCollectionName = createCollectionNameFromURL(url);
         verbose &&
@@ -53,14 +63,23 @@ export async function crawl(options: CrawlerOptions) {
             ),
           );
 
-        await recursiveCrawlFromBaseURL({
-          baseUrl: url,
-          collectionName: newCollectionName,
-          mongoClient,
-          maxDepth,
-          verbose,
-          dryRun,
-        });
+        await recursiveCrawlFromBaseURL(
+          ({ document, title, href }) => {
+            processLangchainDocument({
+              doc: document,
+              title,
+              href,
+              collectionName: newCollectionName,
+              mongoClient,
+              dryRun,
+            });
+          },
+          {
+            baseUrl: url,
+            maxDepth,
+            verbose,
+          },
+        );
       }
     }
     // Otherwise crawl all sources
@@ -71,14 +90,23 @@ export async function crawl(options: CrawlerOptions) {
         );
 
       for (const source of SOURCES) {
-        await recursiveCrawlFromBaseURL({
-          baseUrl: source.url,
-          collectionName: source.collection,
-          mongoClient,
-          maxDepth,
-          verbose,
-          dryRun,
-        });
+        await recursiveCrawlFromBaseURL(
+          ({ document, title, href }) => {
+            processLangchainDocument({
+              doc: document,
+              title,
+              href,
+              collectionName: source.collection,
+              mongoClient,
+              dryRun,
+            });
+          },
+          {
+            baseUrl: source.url,
+            maxDepth,
+            verbose,
+          },
+        );
       }
     }
 
diff --git a/tools/crawler/src/index.ts b/tools/crawler/src/index.ts
index 96eb452de9..fe310f3f2f 100644
--- a/tools/crawler/src/index.ts
+++ b/tools/crawler/src/index.ts
@@ -1,6 +1,15 @@
 export { allowedDomains, MDB_DB, SOURCES } from './constants';
-export { processSingleUrl } from './utils/processSingleUrl';
 export {
+  type LoadedPageContents,
+  loadPageContents,
+} from './utils/loadPageContents';
+export {
+  processLangchainDocument,
+  type ProcessLangchainDocumentOptions as ProcessSingleUrlOptions,
+  type ProcessLangchainDocumentResult as ProcessSingleUrlResult,
+} from './utils/processLangchainDocument';
+export {
+  type CrawlerCallback,
   recursiveCrawlFromBaseURL,
   type RecursiveCrawlOptions,
 } from './utils/recursiveCrawlFromBase';
diff --git a/tools/crawler/src/lambda.ts b/tools/crawler/src/lambda.ts
index 6d4050fcf7..12c23e05ed 100644
--- a/tools/crawler/src/lambda.ts
+++ b/tools/crawler/src/lambda.ts
@@ -1,4 +1,5 @@
 import { connectToMongoDB } from './utils/connectToMongoDB';
+import { processLangchainDocument } from './utils/processLangchainDocument';
 import { recursiveCrawlFromBaseURL } from './utils/recursiveCrawlFromBase';
 import { SOURCES } from './constants';
 
@@ -17,12 +18,20 @@ exports.handler = async (_e: any) => {
 
   try {
     for (const source of SOURCES) {
-      await recursiveCrawlFromBaseURL({
-        baseUrl: source.url,
-        collectionName: source.collection,
-        mongoClient,
-        maxDepth: 3,
-      });
+      await recursiveCrawlFromBaseURL(
+        ({ document, title, href }) =>
+          processLangchainDocument({
+            doc: document,
+            title,
+            href,
+            collectionName: source.collection,
+            mongoClient,
+          }),
+        {
+          baseUrl: source.url,
+          maxDepth: 3,
+        },
+      );
     }
   } catch (error) {
     console.error('Error during crawling:', error);
diff --git a/tools/crawler/src/utils/loadPageContents.ts b/tools/crawler/src/utils/loadPageContents.ts
index 199331a230..0b5593c88a 100644
--- a/tools/crawler/src/utils/loadPageContents.ts
+++ b/tools/crawler/src/utils/loadPageContents.ts
@@ -1,7 +1,7 @@
 import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';
 import { Document as LangChainDocument } from '@langchain/core/documents';
 
-interface LoadedPageContents {
+export interface LoadedPageContents {
   doc: LangChainDocument;
   title: string;
   links: Array<string>;
diff --git a/tools/crawler/src/utils/newURL.ts b/tools/crawler/src/utils/newURL.ts
index 3a55f1511c..663c233176 100644
--- a/tools/crawler/src/utils/newURL.ts
+++ b/tools/crawler/src/utils/newURL.ts
@@ -5,6 +5,6 @@ export const newURL = (href: string) => {
     return new URL(href);
   } catch (error) {
     console.error(chalk.red(`Invalid URL: ${href}`), error);
-    process.exit(1);
+    return null;
   }
 };
diff --git a/tools/crawler/src/utils/processSingleUrl.ts b/tools/crawler/src/utils/processLangchainDocument.ts
similarity index 62%
rename from tools/crawler/src/utils/processSingleUrl.ts
rename to tools/crawler/src/utils/processLangchainDocument.ts
index c56422123e..b46767ede3 100644
--- a/tools/crawler/src/utils/processSingleUrl.ts
+++ b/tools/crawler/src/utils/processLangchainDocument.ts
@@ -1,4 +1,5 @@
 /* eslint-disable no-console */
+import { Document as LangChainDocument } from '@langchain/core/documents';
 import chalk from 'chalk';
 import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
 import { type MongoClient } from 'mongodb';
@@ -6,34 +7,33 @@ import { type MongoClient } from 'mongodb';
 import { CrawlerDocument, CrawlerDocumentWithEmbeddings } from '../types';
 import { vectorize } from '../vectorize';
 
-import { checkRobots } from './checkRobots';
 import { createChecksum } from './createChecksum';
 import { insertDocuments } from './insertDocuments';
-import { loadPageContents } from './loadPageContents';
 import { makeLangChainDocumentMapper } from './mapLangChainDocument';
 
-interface ProcessSingleUrlOptions {
+export interface ProcessLangchainDocumentOptions {
+  doc: LangChainDocument;
+  title: string;
   href: string;
   collectionName: string;
   mongoClient: MongoClient;
+  createEmbeddings?: boolean;
   verbose?: boolean;
   dryRun?: boolean;
 }
 
-interface ProcessSingleUrlResult {
+export interface ProcessLangchainDocumentResult {
   docCount: number;
-  links: Array<string>;
   documents?: Array<CrawlerDocument>;
 }
 
-const _emptyResult = {
+const _emptyResult: ProcessLangchainDocumentResult = {
   docCount: 0,
-  links: [],
 };
 
 /**
- * Process a single URL using LangChain,
- * scrape the page contents,
+ * Process a single LangChain document.
+ *
  * split the text into chunks,
  * creates vector embeddings,
  * and store results in MongoDB.
@@ -41,40 +41,19 @@ const _emptyResult = {
  * Additionally, extracts and returns all links found on the page
  * for further recursive crawling.
  */
-export async function processSingleUrl({
+export async function processLangchainDocument({
+  doc,
+  title,
   href,
   collectionName,
   mongoClient,
+  createEmbeddings = true,
   verbose = false,
   dryRun = false,
-}: ProcessSingleUrlOptions): Promise<ProcessSingleUrlResult> {
+}: ProcessLangchainDocumentOptions): Promise<ProcessLangchainDocumentResult> {
   try {
-    verbose && console.log(chalk.gray(`Processing URL:`), chalk.blue(href));
-    const { hostname, pathname } = new URL(href);
-
-    const isCrawlingAllowed = checkRobots(hostname, pathname, verbose);
-
-    if (!isCrawlingAllowed) {
-      verbose &&
-        console.log(
-          chalk.red(
-            `Crawling disallowed by robots.txt for ${hostname}. Skipping...`,
-          ),
-        );
-      return _emptyResult;
-    }
-
-    const { doc, title, links } = await loadPageContents(href);
-
     verbose &&
-      console.log(
-        chalk.gray(
-          `Loaded document`,
-          `"${chalk.bold(title)}"`,
-          `from`,
-          chalk.blue(href),
-        ),
-      );
+      console.log(chalk.gray(`Loaded document`, `"${chalk.bold(title)}"`));
 
     // Split text into chunks for better processing/storage
     const textSplitter = new RecursiveCharacterTextSplitter({
@@ -88,16 +67,14 @@ export async function processSingleUrl({
 
     const checksum = createChecksum(href, doc.pageContent);
 
-    const documents = chunkedDocs.map(
-      makeLangChainDocumentMapper({ title, href, checksum }),
-    );
+    let documents: Array<CrawlerDocument | CrawlerDocumentWithEmbeddings> =
+      chunkedDocs.map(makeLangChainDocumentMapper({ title, href, checksum }));
 
     if (chunkedDocs.length <= 0) {
       verbose &&
         console.log(chalk.gray(`No content to process for URL: ${href}`));
       return {
         ..._emptyResult,
-        links,
       };
     }
 
@@ -114,12 +91,12 @@ export async function processSingleUrl({
       // For dry run, we'll count the documents that would have been processed
       docCount = documents.length;
     } else {
-      // Create embeddings for each document before inserting into MongoDB
-      verbose && console.log(chalk.blue('Vectorizing documents...'));
+      if (createEmbeddings) {
+        // Create embeddings for each document before inserting into MongoDB
+        verbose && console.log(chalk.blue('Vectorizing documents...'));
 
-      // Process documents one by one to create vector embeddings;
-      const documentsWithEmbeddings: Array<CrawlerDocumentWithEmbeddings> =
-        (await Promise.all(
+        // Process documents one by one to create vector embeddings;
+        documents = (await Promise.all(
           documents.map(async (doc: CrawlerDocument) => {
             try {
               return await vectorize(doc);
@@ -134,12 +111,13 @@ export async function processSingleUrl({
           }),
         )) as Array<CrawlerDocumentWithEmbeddings>;
 
-      verbose &&
-        console.log(
-          chalk.green(
-            `Successfully vectorized ${documentsWithEmbeddings.length} documents`,
-          ),
-        );
+        verbose &&
+          console.log(
+            chalk.green(
+              `Successfully vectorized ${documents.length} documents`,
+            ),
+          );
+      }
     }
 
     await insertDocuments({
@@ -151,7 +129,6 @@ export async function processSingleUrl({
 
     return {
       docCount,
-      links,
       documents,
     };
   } catch (error) {
diff --git a/tools/crawler/src/utils/recursiveCrawlFromBase.ts b/tools/crawler/src/utils/recursiveCrawlFromBase.ts
index 1c7f05ae77..2de86da24c 100644
--- a/tools/crawler/src/utils/recursiveCrawlFromBase.ts
+++ b/tools/crawler/src/utils/recursiveCrawlFromBase.ts
@@ -1,43 +1,46 @@
 /* eslint-disable no-console */
+import { Document as LangChainDocument } from '@langchain/core/documents';
 import chalk from 'chalk';
-import trimEnd from 'lodash/trimEnd';
-import trimStart from 'lodash/trimStart';
-import { type MongoClient } from 'mongodb';
+import { trimEnd, trimStart } from 'lodash-es';
 
 import { allowedDomains, SOURCES } from '../constants';
 
-import { createCollectionNameFromURL } from './createCollectionNameFromURL';
+import { checkRobots } from './checkRobots';
+import { loadPageContents } from './loadPageContents';
 import { newURL } from './newURL';
-import { processSingleUrl } from './processSingleUrl';
+
+export type CrawlerCallback = (arg: {
+  document: LangChainDocument;
+  title: string;
+  href: string;
+  links?: Array<string>;
+}) => void;
 
 export interface RecursiveCrawlOptions {
   baseUrl: string;
-  collectionName: string;
-  mongoClient: MongoClient;
   maxDepth: number;
   relativePath?: string;
   currentDepth?: number;
   visited?: Set<string>;
   verbose?: boolean;
-  dryRun?: boolean;
   enableRecursion?: boolean; // New flag to track if we're crawling an allowed external domain
 }
 
 /**
  * Recursively crawl a website following links up to a maximum depth
  */
-export async function recursiveCrawlFromBaseURL({
-  baseUrl,
-  collectionName,
-  mongoClient,
-  relativePath,
-  maxDepth = 3,
-  currentDepth = 0,
-  visited = new Set<string>(),
-  verbose = false,
-  dryRun = false,
-  enableRecursion = true, // Default to false for initial base URL crawling
-}: RecursiveCrawlOptions): Promise<void> {
+export async function recursiveCrawlFromBaseURL(
+  callback: CrawlerCallback,
+  {
+    baseUrl,
+    relativePath,
+    maxDepth = 3,
+    currentDepth = 0,
+    visited = new Set<string>(),
+    verbose = false,
+    enableRecursion = true, // Default to false for initial base URL crawling
+  }: RecursiveCrawlOptions,
+): Promise<void> {
   currentDepth === 0 &&
     verbose &&
     console.log(chalk.green('\nStarting crawl of ' + baseUrl));
@@ -49,6 +52,13 @@ export async function recursiveCrawlFromBaseURL({
 
   const fullUrl = newURL(fullHref);
 
+  if (!fullUrl) {
+    verbose && console.log(chalk.red(`Invalid URL. Skipping ${fullHref}`));
+    return;
+  }
+
+  const { hostname, pathname } = fullUrl;
+
   // Don't crawl if we've reached max depth or already visited this URL
   if (currentDepth > maxDepth || visited.has(fullUrl.href)) {
     verbose &&
@@ -87,13 +97,26 @@ export async function recursiveCrawlFromBaseURL({
   // Mark this URL as visited
   visited.add(fullUrl.href);
 
-  // Process the current URL
-  const { links } = await processSingleUrl({
-    href: fullUrl.href,
-    collectionName,
-    mongoClient,
-    verbose,
-    dryRun,
+  // Start crawling the page
+  const isCrawlingAllowed = checkRobots(hostname, pathname, verbose);
+
+  if (!isCrawlingAllowed) {
+    verbose &&
+      console.log(
+        chalk.red(
+          `Crawling disallowed by robots.txt for ${hostname}. Skipping...`,
+        ),
+      );
+    return;
+  }
+
+  const { doc, title, links } = await loadPageContents(fullHref);
+
+  callback({
+    document: doc,
+    title,
+    href: fullHref,
+    links,
   });
 
   // If we've reached max depth, don't crawl further.
@@ -118,6 +141,11 @@ export async function recursiveCrawlFromBaseURL({
         ? newURL(fullUrl.origin + '/' + trimStart(link, '/'))
         : newURL(link);
 
+      if (!linkUrl) {
+        verbose && console.log(chalk.red(`Invalid link. Skipping ${link}`));
+        continue;
+      }
+
       const isVisited = visited.has(linkUrl.href);
       const isSameDomain = link.startsWith(baseUrl);
       const isExternalLink = !isRelative && !isSameDomain;
@@ -152,21 +180,14 @@ export async function recursiveCrawlFromBaseURL({
           ),
         );
 
-      const _collectionName = isExternalLink
-        ? createCollectionNameFromURL(linkUrl.href)
-        : collectionName;
-
       // For internal links, continue with original base URL
-      await recursiveCrawlFromBaseURL({
+      await recursiveCrawlFromBaseURL(callback, {
         baseUrl: linkUrl.origin,
         relativePath: linkUrl.pathname,
-        collectionName: _collectionName,
-        mongoClient,
         maxDepth,
         currentDepth: currentDepth + 1,
         visited,
         verbose,
-        dryRun,
         enableRecursion: !isExternalLink,
       });
     }
diff --git a/tools/crawler/tsconfig.json b/tools/crawler/tsconfig.json
index 43b3ae1f8c..e209b51503 100644
--- a/tools/crawler/tsconfig.json
+++ b/tools/crawler/tsconfig.json
@@ -3,11 +3,8 @@
   "compilerOptions": {
     "outDir": "./dist",
     "rootDir": "./src",
-    "composite": true,
-    "declaration": true,
     "declarationDir": "./dist/types",
-    "sourceMap": true,
-    "strict": true,
-    "skipLibCheck": true
-  }
+  },
+  "include": ["./src/**/*.ts"],
+
 }


Reviews:
user: login: tsck
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T19:07:04Z
user: login: TheSonOfThomp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T19:33:32Z
user: login: TheSonOfThomp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T19:34:19Z
user: login: tsck
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-09T14:36:23Z

Review comments:
path: apps/chatbot-server/tsconfig.json
user: login: tsck
body: Got an unresolved merge conflict here.
created_at: 2025-05-08T19:05:25Z
author_association: COLLABORATOR
path: apps/chatbot-server/tsconfig.json
user: login: TheSonOfThomp
body: oh weird. how'd it let me commit that 
created_at: 2025-05-08T19:33:32Z
author_association: COLLABORATOR
path: apps/chatbot-server/tsconfig.json
user: login: TheSonOfThomp
body: good now 
created_at: 2025-05-08T19:34:19Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-08T18:01:10Z
body: ###  ⚠️  No Changeset found

Latest commit: 329b39e9809b90974e5fcaea2a39a52671ec8201

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/mongodb/leafygreen-ui/new/a/ingest-web-loader?filename=.changeset/soft-gorillas-melt.md&value=---%0A%22%40leafygreen-ui%2Fchatbot-server%22%3A%20patch%0A%22%40lg-tools%2Fcrawler%22%3A%20patch%0A---%0A%0AIngest%20with%20recursive%20crawler%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-08T18:30:01Z
body: **Size Change:** -105 B (-0.01%) 

**Total Size:** 1.53 MB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `tools/crawler/dist/cli.js` | 8.3 kB | +51 B (+0.62%) |
| `tools/crawler/dist/esm/index.js` | 6.71 kB | -62 B (-0.91%) |
| `tools/crawler/dist/index.js` | 6.88 kB | -67 B (-0.96%) |
| `tools/crawler/dist/lambda.js` | 7.28 kB | -27 B (-0.37%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `charts/chart-card/dist/esm/index.js` | 2.78 kB |
| `charts/chart-card/dist/index.js` | 2.93 kB |
| `charts/colors/dist/esm/index.js` | 515 B |
| `charts/colors/dist/index.js` | 640 B |
| `charts/core/dist/esm/index.js` | 16 kB |
| `charts/core/dist/index.js` | 16.2 kB |
| `charts/drag-provider/dist/esm/index.js` | 1.39 kB |
| `charts/drag-provider/dist/index.js` | 1.5 kB |
| `charts/legend/dist/esm/index.js` | 1.55 kB |
| `charts/legend/dist/index.js` | 1.71 kB |
| `charts/series-provider/dist/esm/index.js` | 1.05 kB |
| `charts/series-provider/dist/index.js` | 1.17 kB |
| `chat/avatar/dist/esm/index.js` | 1.36 kB |
| `chat/avatar/dist/index.js` | 1.52 kB |
| `chat/chat-disclaimer/dist/esm/index.js` | 566 B |
| `chat/chat-disclaimer/dist/index.js` | 755 B |
| `chat/chat-window/dist/esm/index.js` | 1.62 kB |
| `chat/chat-window/dist/index.js` | 1.79 kB |
| `chat/fixed-chat-window/dist/esm/index.js` | 2.39 kB |
| `chat/fixed-chat-window/dist/index.js` | 2.57 kB |
| `chat/input-bar/dist/esm/index.js` | 6.9 kB |
| `chat/input-bar/dist/index.js` | 7.11 kB |
| `chat/leafygreen-chat-provider/dist/esm/index.js` | 873 B |
| `chat/leafygreen-chat-provider/dist/index.js` | 1.02 kB |
| `chat/lg-markdown/dist/esm/index.js` | 1.44 kB |
| `chat/lg-markdown/dist/index.js` | 1.59 kB |
| `chat/message-feed/dist/esm/index.js` | 2.78 kB |
| `chat/message-feed/dist/index.js` | 2.97 kB |
| `chat/message-feedback/dist/esm/index.js` | 2.54 kB |
| `chat/message-feedback/dist/index.js` | 2.78 kB |
| `chat/message-prompts/dist/esm/index.js` | 1.72 kB |
| `chat/message-prompts/dist/index.js` | 1.86 kB |
| `chat/message-rating/dist/esm/index.js` | 2.55 kB |
| `chat/message-rating/dist/index.js` | 2.75 kB |
| `chat/message/dist/esm/index.js` | 3.78 kB |
| `chat/message/dist/index.js` | 3.98 kB |
| `chat/rich-links/dist/esm/index.js` | 2.68 kB |
| `chat/rich-links/dist/index.js` | 2.89 kB |
| `chat/title-bar/dist/esm/index.js` | 1.37 kB |
| `chat/title-bar/dist/index.js` | 1.58 kB |
| `packages/a11y/dist/esm/index.js` | 1.7 kB |
| `packages/a11y/dist/index.js` | 1.83 kB |
| `packages/avatar/dist/esm/index.js` | 2.35 kB |
| `packages/avatar/dist/index.js` | 2.53 kB |
| `packages/badge/dist/esm/index.js` | 1.41 kB |
| `packages/badge/dist/index.js` | 1.59 kB |
| `packages/banner/dist/esm/index.js` | 3.08 kB |
| `packages/banner/dist/index.js` | 3.37 kB |
| `packages/box/dist/esm/index.js` | 489 B |
| `packages/box/dist/index.js` | 627 B |
| `packages/button/dist/esm/index.js` | 4.92 kB |
| `packages/button/dist/index.js` | 5.1 kB |
| `packages/callout/dist/esm/index.js` | 1.54 kB |
| `packages/callout/dist/index.js` | 1.71 kB |
| `packages/card/dist/esm/index.js` | 1.51 kB |
| `packages/card/dist/index.js` | 1.69 kB |
| `packages/checkbox/dist/esm/index.js` | 4.23 kB |
| `packages/checkbox/dist/index.js` | 4.43 kB |
| `packages/chip/dist/esm/index.js` | 3.2 kB |
| `packages/chip/dist/index.js` | 3.41 kB |
| `packages/code/dist/esm/index.js` | 10.9 kB |
| `packages/code/dist/index.js` | 11.5 kB |
| `packages/combobox/dist/esm/index.js` | 11.6 kB |
| `packages/combobox/dist/index.js` | 11.8 kB |
| `packages/confirmation-modal/dist/esm/index.js` | 2.52 kB |
| `packages/confirmation-modal/dist/index.js` | 2.74 kB |
| `packages/copyable/dist/esm/index.js` | 2.78 kB |
| `packages/copyable/dist/index.js` | 2.99 kB |
| `packages/date-picker/dist/esm/index.js` | 13.6 kB |
| `packages/date-picker/dist/index.js` | 14 kB |
| `packages/date-utils/dist/esm/index.js` | 3.48 kB |
| `packages/date-utils/dist/index.js` | 3.6 kB |
| `packages/descendants/dist/esm/index.js` | 2.83 kB |
| `packages/descendants/dist/index.js` | 2.93 kB |
| `packages/drawer/dist/esm/index.js` | 3.62 kB |
| `packages/drawer/dist/index.js` | 3.75 kB |
| `packages/emotion/dist/esm/index.js` | 356 B |
| `packages/emotion/dist/index.js` | 572 B |
| `packages/empty-state/dist/esm/index.js` | 1.77 kB |
| `packages/empty-state/dist/index.js` | 1.94 kB |
| `packages/expandable-card/dist/esm/index.js` | 2.85 kB |
| `packages/expandable-card/dist/index.js` | 3.03 kB |
| `packages/form-field/dist/esm/index.js` | 4.31 kB |
| `packages/form-field/dist/index.js` | 4.47 kB |
| `packages/form-footer/dist/esm/index.js` | 1.71 kB |
| `packages/form-footer/dist/index.js` | 1.91 kB |
| `packages/gallery-indicator/dist/esm/index.js` | 1.51 kB |
| `packages/gallery-indicator/dist/index.js` | 1.67 kB |
| `packages/guide-cue/dist/esm/index.js` | 6.37 kB |
| `packages/guide-cue/dist/index.js` | 6.57 kB |
| `packages/hooks/dist/esm/index.js` | 3.45 kB |
| `packages/hooks/dist/index.js` | 3.59 kB |
| `packages/icon-button/dist/esm/index.js` | 2.23 kB |
| `packages/icon-button/dist/index.js` | 2.43 kB |
| `packages/icon/dist/ActivityFeed.js` | 1.95 kB |
| `packages/icon/dist/AddFile.js` | 1.72 kB |
| `packages/icon/dist/AllProducts.js` | 1.79 kB |
| `packages/icon/dist/AnalyticsNode.js` | 1.56 kB |
| `packages/icon/dist/Apps.js` | 1.39 kB |
| `packages/icon/dist/Array.js` | 1.51 kB |
| `packages/icon/dist/ArrowDown.js` | 1.64 kB |
| `packages/icon/dist/ArrowLeft.js` | 1.65 kB |
| `packages/icon/dist/ArrowRight.js` | 1.64 kB |
| `packages/icon/dist/ArrowUp.js` | 1.64 kB |
| `packages/icon/dist/Beaker.js` | 2.07 kB |
| `packages/icon/dist/Bell.js` | 1.6 kB |
| `packages/icon/dist/Biometric.js` | 2.17 kB |
| `packages/icon/dist/Boolean.js` | 1.44 kB |
| `packages/icon/dist/Building.js` | 1.59 kB |
| `packages/icon/dist/Bulb.js` | 1.64 kB |
| `packages/icon/dist/Calendar.js` | 1.59 kB |
| `packages/icon/dist/Camera.js` | 1.68 kB |
| `packages/icon/dist/Cap.js` | 1.8 kB |
| `packages/icon/dist/CaretDown.js` | 1.46 kB |
| `packages/icon/dist/CaretLeft.js` | 1.46 kB |
| `packages/icon/dist/CaretRight.js` | 1.46 kB |
| `packages/icon/dist/CaretUp.js` | 1.46 kB |
| `packages/icon/dist/ChartFilled.js` | 1.56 kB |
| `packages/icon/dist/Charts.js` | 1.53 kB |
| `packages/icon/dist/Checkmark.js` | 1.6 kB |
| `packages/icon/dist/CheckmarkWithCircle.js` | 1.67 kB |
| `packages/icon/dist/ChevronDown.js` | 1.56 kB |
| `packages/icon/dist/ChevronLeft.js` | 1.56 kB |
| `packages/icon/dist/ChevronRight.js` | 1.57 kB |
| `packages/icon/dist/ChevronUp.js` | 1.55 kB |
| `packages/icon/dist/Circle.js` | 1.33 kB |
| `packages/icon/dist/Clock.js` | 1.62 kB |
| `packages/icon/dist/ClockWithArrow.js` | 1.91 kB |
| `packages/icon/dist/Clone.js` | 1.5 kB |
| `packages/icon/dist/Cloud.js` | 1.67 kB |
| `packages/icon/dist/Code.js` | 1.88 kB |
| `packages/icon/dist/CodeBlock.js` | 1.81 kB |
| `packages/icon/dist/Coin.js` | 1.93 kB |
| `packages/icon/dist/Colon.js` | 1.44 kB |
| `packages/icon/dist/Config.js` | 1.76 kB |
| `packages/icon/dist/Connect.js` | 1.92 kB |
| `packages/icon/dist/Copy.js` | 1.87 kB |
| `packages/icon/dist/CreditCard.js` | 1.42 kB |
| `packages/icon/dist/CurlyBraces.js` | 2.02 kB |
| `packages/icon/dist/Cursor.js` | 1.55 kB |
| `packages/icon/dist/Dashboard.js` | 1.56 kB |
| `packages/icon/dist/Database.js` | 2.02 kB |
| `packages/icon/dist/Diagram.js` | 1.64 kB |
| `packages/icon/dist/Diagram2.js` | 1.65 kB |
| `packages/icon/dist/Diagram3.js` | 1.62 kB |
| `packages/icon/dist/Disconnect.js` | 1.86 kB |
| `packages/icon/dist/Download.js` | 1.78 kB |
| `packages/icon/dist/Drag.js` | 1.6 kB |
| `packages/icon/dist/Edit.js` | 1.51 kB |
| `packages/icon/dist/Ellipsis.js` | 1.48 kB |
| `packages/icon/dist/Email.js` | 1.7 kB |
| `packages/icon/dist/EmptyDatabase.js` | 4.29 kB |
| `packages/icon/dist/EmptyFolder.js` | 2.08 kB |
| `packages/icon/dist/Eraser.js` | 1.77 kB |
| `packages/icon/dist/Escalation.js` | 1.67 kB |
| `packages/icon/dist/esm/ActivityFeed.js` | 1.74 kB |
| `packages/icon/dist/esm/AddFile.js` | 1.51 kB |
| `packages/icon/dist/esm/AllProducts.js` | 1.59 kB |
| `packages/icon/dist/esm/AnalyticsNode.js` | 1.36 kB |
| `packages/icon/dist/esm/Apps.js` | 1.2 kB |
| `packages/icon/dist/esm/Array.js` | 1.31 kB |
| `packages/icon/dist/esm/ArrowDown.js` | 1.44 kB |
| `packages/icon/dist/esm/ArrowLeft.js` | 1.44 kB |
| `packages/icon/dist/esm/ArrowRight.js` | 1.43 kB |
| `packages/icon/dist/esm/ArrowUp.js` | 1.43 kB |
| `packages/icon/dist/esm/Beaker.js` | 1.86 kB |
| `packages/icon/dist/esm/Bell.js` | 1.4 kB |
| `packages/icon/dist/esm/Biometric.js` | 1.96 kB |
| `packages/icon/dist/esm/Boolean.js` | 1.24 kB |
| `packages/icon/dist/esm/Building.js` | 1.39 kB |
| `packages/icon/dist/esm/Bulb.js` | 1.44 kB |
| `packages/icon/dist/esm/Calendar.js` | 1.39 kB |
| `packages/icon/dist/esm/Camera.js` | 1.48 kB |
| `packages/icon/dist/esm/Cap.js` | 1.59 kB |
| `packages/icon/dist/esm/CaretDown.js` | 1.26 kB |
| `packages/icon/dist/esm/CaretLeft.js` | 1.26 kB |
| `packages/icon/dist/esm/CaretRight.js` | 1.26 kB |
| `packages/icon/dist/esm/CaretUp.js` | 1.26 kB |
| `packages/icon/dist/esm/ChartFilled.js` | 1.35 kB |
| `packages/icon/dist/esm/Charts.js` | 1.33 kB |
| `packages/icon/dist/esm/Checkmark.js` | 1.4 kB |
| `packages/icon/dist/esm/CheckmarkWithCircle.js` | 1.46 kB |
| `packages/icon/dist/esm/ChevronDown.js` | 1.36 kB |
| `packages/icon/dist/esm/ChevronLeft.js` | 1.36 kB |
| `packages/icon/dist/esm/ChevronRight.js` | 1.38 kB |
| `packages/icon/dist/esm/ChevronUp.js` | 1.35 kB |
| `packages/icon/dist/esm/Circle.js` | 1.13 kB |
| `packages/icon/dist/esm/Clock.js` | 1.42 kB |
| `packages/icon/dist/esm/ClockWithArrow.js` | 1.7 kB |
| `packages/icon/dist/esm/Clone.js` | 1.29 kB |
| `packages/icon/dist/esm/Cloud.js` | 1.47 kB |
| `packages/icon/dist/esm/Code.js` | 1.68 kB |
| `packages/icon/dist/esm/CodeBlock.js` | 1.61 kB |
| `packages/icon/dist/esm/Coin.js` | 1.72 kB |
| `packages/icon/dist/esm/Colon.js` | 1.25 kB |
| `packages/icon/dist/esm/Config.js` | 1.55 kB |
| `packages/icon/dist/esm/Connect.js` | 1.72 kB |
| `packages/icon/dist/esm/Copy.js` | 1.67 kB |
| `packages/icon/dist/esm/CreditCard.js` | 1.22 kB |
| `packages/icon/dist/esm/CurlyBraces.js` | 1.82 kB |
| `packages/icon/dist/esm/Cursor.js` | 1.35 kB |
| `packages/icon/dist/esm/Dashboard.js` | 1.36 kB |
| `packages/icon/dist/esm/Database.js` | 1.81 kB |
| `packages/icon/dist/esm/Diagram.js` | 1.44 kB |
| `packages/icon/dist/esm/Diagram2.js` | 1.44 kB |
| `packages/icon/dist/esm/Diagram3.js` | 1.42 kB |
| `packages/icon/dist/esm/Disconnect.js` | 1.65 kB |
| `packages/icon/dist/esm/Download.js` | 1.57 kB |
| `packages/icon/dist/esm/Drag.js` | 1.41 kB |
| `packages/icon/dist/esm/Edit.js` | 1.31 kB |
| `packages/icon/dist/esm/Ellipsis.js` | 1.29 kB |
| `packages/icon/dist/esm/Email.js` | 1.5 kB |
| `packages/icon/dist/esm/EmptyDatabase.js` | 4.07 kB |
| `packages/icon/dist/esm/EmptyFolder.js` | 1.88 kB |
| `packages/icon/dist/esm/Eraser.js` | 1.56 kB |
| `packages/icon/dist/esm/Escalation.js` | 1.46 kB |
| `packages/icon/dist/esm/Export.js` | 1.57 kB |
| `packages/icon/dist/esm/Favorite.js` | 1.52 kB |
| `packages/icon/dist/esm/Federation.js` | 1.84 kB |
| `packages/icon/dist/esm/File.js` | 1.33 kB |
| `packages/icon/dist/esm/Filter.js` | 1.32 kB |
| `packages/icon/dist/esm/Folder.js` | 1.23 kB |
| `packages/icon/dist/esm/Format.js` | 1.65 kB |
| `packages/icon/dist/esm/FullScreenEnter.js` | 1.48 kB |
| `packages/icon/dist/esm/FullScreenExit.js` | 1.49 kB |
| `packages/icon/dist/esm/Function.js` | 2.53 kB |
| `packages/icon/dist/esm/Gauge.js` | 1.52 kB |
| `packages/icon/dist/esm/GlobeAmericas.js` | 1.44 kB |
| `packages/icon/dist/esm/GovernmentBuilding.js` | 1.47 kB |
| `packages/icon/dist/esm/Guide.js` | 1.78 kB |
| `packages/icon/dist/esm/Hash.js` | 1.62 kB |
| `packages/icon/dist/esm/HiddenSecondaryNode.js` | 2.89 kB |
| `packages/icon/dist/esm/Highlight.js` | 1.67 kB |
| `packages/icon/dist/esm/Home.js` | 1.58 kB |
| `packages/icon/dist/esm/HorizontalDrag.js` | 1.41 kB |
| `packages/icon/dist/esm/Import.js` | 1.56 kB |
| `packages/icon/dist/esm/ImportantWithCircle.js` | 1.33 kB |
| `packages/icon/dist/esm/index.js` | 35.2 kB |
| `packages/icon/dist/esm/InfoWithCircle.js` | 1.36 kB |
| `packages/icon/dist/esm/InternalEmployee.js` | 1.65 kB |
| `packages/icon/dist/esm/InviteUser.js` | 1.67 kB |
| `packages/icon/dist/esm/Key.js` | 1.47 kB |
| `packages/icon/dist/esm/Laptop.js` | 1.46 kB |
| `packages/icon/dist/esm/LightningBolt.js` | 1.33 kB |
| `packages/icon/dist/esm/Link.js` | 1.83 kB |
| `packages/icon/dist/esm/List.js` | 1.47 kB |
| `packages/icon/dist/esm/Lock.js` | 1.43 kB |
| `packages/icon/dist/esm/LogIn.js` | 1.5 kB |
| `packages/icon/dist/esm/LogOut.js` | 1.56 kB |
| `packages/icon/dist/esm/MagnifyingGlass.js` | 1.4 kB |
| `packages/icon/dist/esm/Megaphone.js` | 1.37 kB |
| `packages/icon/dist/esm/Menu.js` | 1.28 kB |
| `packages/icon/dist/esm/Minus.js` | 1.23 kB |
| `packages/icon/dist/esm/Mobile.js` | 1.23 kB |
| `packages/icon/dist/esm/Moon.js` | 1.41 kB |
| `packages/icon/dist/esm/MultiDirectionArrow.js` | 1.45 kB |
| `packages/icon/dist/esm/MultiLayers.js` | 2.4 kB |
| `packages/icon/dist/esm/NavCollapse.js` | 1.47 kB |
| `packages/icon/dist/esm/NavExpand.js` | 1.48 kB |
| `packages/icon/dist/esm/NoFilter.js` | 1.46 kB |
| `packages/icon/dist/esm/NotAllowed.js` | 1.35 kB |
| `packages/icon/dist/esm/Note.js` | 1.38 kB |
| `packages/icon/dist/esm/NumberedList.js` | 2.28 kB |
| `packages/icon/dist/esm/OpenNewTab.js` | 1.66 kB |
| `packages/icon/dist/esm/OutlineFavorite.js` | 1.79 kB |
| `packages/icon/dist/esm/Package.js` | 1.64 kB |
| `packages/icon/dist/esm/Pause.js` | 1.28 kB |
| `packages/icon/dist/esm/Pending.js` | 1.26 kB |
| `packages/icon/dist/esm/Person.js` | 1.45 kB |
| `packages/icon/dist/esm/PersonGroup.js` | 1.65 kB |
| `packages/icon/dist/esm/PersonWithLock.js` | 1.66 kB |
| `packages/icon/dist/esm/Pin.js` | 1.39 kB |
| `packages/icon/dist/esm/Play.js` | 1.27 kB |
| `packages/icon/dist/esm/Plus.js` | 1.29 kB |
| `packages/icon/dist/esm/PlusWithCircle.js` | 1.32 kB |
| `packages/icon/dist/esm/Primary.js` | 1.34 kB |
| `packages/icon/dist/esm/Project.js` | 1.38 kB |
| `packages/icon/dist/esm/QuestionMarkWithCircle.js` | 1.69 kB |
| `packages/icon/dist/esm/Read.js` | 1.98 kB |
| `packages/icon/dist/esm/Recommended.js` | 2.16 kB |
| `packages/icon/dist/esm/Redo.js` | 1.63 kB |
| `packages/icon/dist/esm/Refresh.js` | 1.7 kB |
| `packages/icon/dist/esm/Relationship.js` | 1.39 kB |
| `packages/icon/dist/esm/ReplicaSet.js` | 1.54 kB |
| `packages/icon/dist/esm/Resize.js` | 1.36 kB |
| `packages/icon/dist/esm/Resource.js` | 1.69 kB |
| `packages/icon/dist/esm/Return.js` | 1.46 kB |
| `packages/icon/dist/esm/Revert.js` | 1.5 kB |
| `packages/icon/dist/esm/Router.js` | 2.16 kB |
| `packages/icon/dist/esm/Save.js` | 1.89 kB |
| `packages/icon/dist/esm/SearchIndex.js` | 2.02 kB |
| `packages/icon/dist/esm/Secondary.js` | 1.58 kB |
| `packages/icon/dist/esm/Serverless.js` | 1.53 kB |
| `packages/icon/dist/esm/Settings.js` | 1.96 kB |
| `packages/icon/dist/esm/ShardedCluster.js` | 1.91 kB |
| `packages/icon/dist/esm/Shell.js` | 1.45 kB |
| `packages/icon/dist/esm/Shirt.js` | 1.4 kB |
| `packages/icon/dist/esm/SMS.js` | 1.41 kB |
| `packages/icon/dist/esm/SortAscending.js` | 1.49 kB |
| `packages/icon/dist/esm/SortDescending.js` | 1.48 kB |
| `packages/icon/dist/esm/Sparkle.js` | 1.81 kB |
| `packages/icon/dist/esm/SplitHorizontal.js` | 1.25 kB |
| `packages/icon/dist/esm/SplitVertical.js` | 1.25 kB |
| `packages/icon/dist/esm/Stitch.js` | 1.29 kB |
| `packages/icon/dist/esm/Stop.js` | 1.14 kB |
| `packages/icon/dist/esm/Streaming.js` | 2.28 kB |
| `packages/icon/dist/esm/String.js` | 1.4 kB |
| `packages/icon/dist/esm/Sun.js` | 1.62 kB |
| `packages/icon/dist/esm/Support.js` | 1.48 kB |
| `packages/icon/dist/esm/Sweep.js` | 1.5 kB |
| `packages/icon/dist/esm/Table.js` | 1.28 kB |
| `packages/icon/dist/esm/Tag.js` | 1.31 kB |
| `packages/icon/dist/esm/TemporaryTable.js` | 1.51 kB |
| `packages/icon/dist/esm/ThumbsDown.js` | 1.6 kB |
| `packages/icon/dist/esm/ThumbsUp.js` | 1.6 kB |
| `packages/icon/dist/esm/TimeSeries.js` | 1.64 kB |
| `packages/icon/dist/esm/TimeSeriesCollection.js` | 1.78 kB |
| `packages/icon/dist/esm/Trash.js` | 1.31 kB |
| `packages/icon/dist/esm/Undo.js` | 1.62 kB |
| `packages/icon/dist/esm/University.js` | 1.81 kB |
| `packages/icon/dist/esm/Unlock.js` | 1.5 kB |
| `packages/icon/dist/esm/Unsorted.js` | 1.53 kB |
| `packages/icon/dist/esm/UpDownCarets.js` | 1.38 kB |
| `packages/icon/dist/esm/Upload.js` | 1.59 kB |
| `packages/icon/dist/esm/VerticalEllipsis.js` | 1.3 kB |
| `packages/icon/dist/esm/View.js` | 1.44 kB |
| `packages/icon/dist/esm/Visibility.js` | 1.61 kB |
| `packages/icon/dist/esm/VisibilityOff.js` | 2.01 kB |
| `packages/icon/dist/esm/Warning.js` | 1.38 kB |
| `packages/icon/dist/esm/Wizard.js` | 1.72 kB |
| `packages/icon/dist/esm/Wrench.js` | 1.72 kB |
| `packages/icon/dist/esm/Write.js` | 1.99 kB |
| `packages/icon/dist/esm/X.js` | 1.42 kB |
| `packages/icon/dist/esm/XWithCircle.js` | 1.36 kB |
| `packages/icon/dist/Export.js` | 1.78 kB |
| `packages/icon/dist/Favorite.js` | 1.73 kB |
| `packages/icon/dist/Federation.js` | 2.05 kB |
| `packages/icon/dist/File.js` | 1.53 kB |
| `packages/icon/dist/Filter.js` | 1.52 kB |
| `packages/icon/dist/Folder.js` | 1.43 kB |
| `packages/icon/dist/Format.js` | 1.85 kB |
| `packages/icon/dist/FullScreenEnter.js` | 1.68 kB |
| `packages/icon/dist/FullScreenExit.js` | 1.69 kB |
| `packages/icon/dist/Function.js` | 2.74 kB |
| `packages/icon/dist/Gauge.js` | 1.72 kB |
| `packages/icon/dist/GlobeAmericas.js` | 1.64 kB |
| `packages/icon/dist/GovernmentBuilding.js` | 1.67 kB |
| `packages/icon/dist/Guide.js` | 1.99 kB |
| `packages/icon/dist/Hash.js` | 1.83 kB |
| `packages/icon/dist/HiddenSecondaryNode.js` | 3.1 kB |
| `packages/icon/dist/Highlight.js` | 1.87 kB |
| `packages/icon/dist/Home.js` | 1.79 kB |
| `packages/icon/dist/HorizontalDrag.js` | 1.61 kB |
| `packages/icon/dist/Import.js` | 1.77 kB |
| `packages/icon/dist/ImportantWithCircle.js` | 1.53 kB |
| `packages/icon/dist/index.js` | 35.5 kB |
| `packages/icon/dist/InfoWithCircle.js` | 1.56 kB |
| `packages/icon/dist/InternalEmployee.js` | 1.86 kB |
| `packages/icon/dist/InviteUser.js` | 1.87 kB |
| `packages/icon/dist/Key.js` | 1.67 kB |
| `packages/icon/dist/Laptop.js` | 1.65 kB |
| `packages/icon/dist/LightningBolt.js` | 1.53 kB |
| `packages/icon/dist/Link.js` | 2.04 kB |
| `packages/icon/dist/List.js` | 1.67 kB |
| `packages/icon/dist/Lock.js` | 1.63 kB |
| `packages/icon/dist/LogIn.js` | 1.7 kB |
| `packages/icon/dist/LogOut.js` | 1.76 kB |
| `packages/icon/dist/MagnifyingGlass.js` | 1.6 kB |
| `packages/icon/dist/Megaphone.js` | 1.57 kB |
| `packages/icon/dist/Menu.js` | 1.48 kB |
| `packages/icon/dist/Minus.js` | 1.43 kB |
| `packages/icon/dist/Mobile.js` | 1.43 kB |
| `packages/icon/dist/Moon.js` | 1.62 kB |
| `packages/icon/dist/MultiDirectionArrow.js` | 1.65 kB |
| `packages/icon/dist/MultiLayers.js` | 2.62 kB |
| `packages/icon/dist/NavCollapse.js` | 1.67 kB |
| `packages/icon/dist/NavExpand.js` | 1.68 kB |
| `packages/icon/dist/NoFilter.js` | 1.66 kB |
| `packages/icon/dist/NotAllowed.js` | 1.55 kB |
| `packages/icon/dist/Note.js` | 1.58 kB |
| `packages/icon/dist/NumberedList.js` | 2.48 kB |
| `packages/icon/dist/OpenNewTab.js` | 1.87 kB |
| `packages/icon/dist/OutlineFavorite.js` | 2 kB |
| `packages/icon/dist/Package.js` | 1.84 kB |
| `packages/icon/dist/Pause.js` | 1.49 kB |
| `packages/icon/dist/Pending.js` | 1.46 kB |
| `packages/icon/dist/Person.js` | 1.65 kB |
| `packages/icon/dist/PersonGroup.js` | 1.86 kB |
| `packages/icon/dist/PersonWithLock.js` | 1.86 kB |
| `packages/icon/dist/Pin.js` | 1.59 kB |
| `packages/icon/dist/Play.js` | 1.47 kB |
| `packages/icon/dist/Plus.js` | 1.49 kB |
| `packages/icon/dist/PlusWithCircle.js` | 1.52 kB |
| `packages/icon/dist/Primary.js` | 1.54 kB |
| `packages/icon/dist/Project.js` | 1.58 kB |
| `packages/icon/dist/QuestionMarkWithCircle.js` | 1.9 kB |
| `packages/icon/dist/Read.js` | 2.19 kB |
| `packages/icon/dist/Recommended.js` | 2.37 kB |
| `packages/icon/dist/Redo.js` | 1.84 kB |
| `packages/icon/dist/Refresh.js` | 1.9 kB |
| `packages/icon/dist/Relationship.js` | 1.59 kB |
| `packages/icon/dist/ReplicaSet.js` | 1.75 kB |
| `packages/icon/dist/Resize.js` | 1.56 kB |
| `packages/icon/dist/Resource.js` | 1.89 kB |
| `packages/icon/dist/Return.js` | 1.66 kB |
| `packages/icon/dist/Revert.js` | 1.7 kB |
| `packages/icon/dist/Router.js` | 2.37 kB |
| `packages/icon/dist/Save.js` | 2.09 kB |
| `packages/icon/dist/SearchIndex.js` | 2.22 kB |
| `packages/icon/dist/Secondary.js` | 1.78 kB |
| `packages/icon/dist/Serverless.js` | 1.74 kB |
| `packages/icon/dist/Settings.js` | 2.16 kB |
| `packages/icon/dist/ShardedCluster.js` | 2.12 kB |
| `packages/icon/dist/Shell.js` | 1.65 kB |
| `packages/icon/dist/Shirt.js` | 1.6 kB |
| `packages/icon/dist/SMS.js` | 1.6 kB |
| `packages/icon/dist/SortAscending.js` | 1.69 kB |
| `packages/icon/dist/SortDescending.js` | 1.69 kB |
| `packages/icon/dist/Sparkle.js` | 2.02 kB |
| `packages/icon/dist/SplitHorizontal.js` | 1.45 kB |
| `packages/icon/dist/SplitVertical.js` | 1.45 kB |
| `packages/icon/dist/Stitch.js` | 1.49 kB |
| `packages/icon/dist/Stop.js` | 1.33 kB |
| `packages/icon/dist/Streaming.js` | 2.49 kB |
| `packages/icon/dist/String.js` | 1.6 kB |
| `packages/icon/dist/Sun.js` | 1.81 kB |
| `packages/icon/dist/Support.js` | 1.69 kB |
| `packages/icon/dist/Sweep.js` | 1.7 kB |
| `packages/icon/dist/Table.js` | 1.48 kB |
| `packages/icon/dist/Tag.js` | 1.51 kB |
| `packages/icon/dist/TemporaryTable.js` | 1.72 kB |
| `packages/icon/dist/ThumbsDown.js` | 1.81 kB |
| `packages/icon/dist/ThumbsUp.js` | 1.8 kB |
| `packages/icon/dist/TimeSeries.js` | 1.86 kB |
| `packages/icon/dist/TimeSeriesCollection.js` | 1.99 kB |
| `packages/icon/dist/Trash.js` | 1.51 kB |
| `packages/icon/dist/Undo.js` | 1.83 kB |
| `packages/icon/dist/University.js` | 2.02 kB |
| `packages/icon/dist/Unlock.js` | 1.7 kB |
| `packages/icon/dist/Unsorted.js` | 1.74 kB |
| `packages/icon/dist/UpDownCarets.js` | 1.59 kB |
| `packages/icon/dist/Upload.js` | 1.79 kB |
| `packages/icon/dist/VerticalEllipsis.js` | 1.5 kB |
| `packages/icon/dist/View.js` | 1.64 kB |
| `packages/icon/dist/Visibility.js` | 1.81 kB |
| `packages/icon/dist/VisibilityOff.js` | 2.22 kB |
| `packages/icon/dist/Warning.js` | 1.58 kB |
| `packages/icon/dist/Wizard.js` | 1.93 kB |
| `packages/icon/dist/Wrench.js` | 1.92 kB |
| `packages/icon/dist/Write.js` | 2.2 kB |
| `packages/icon/dist/X.js` | 1.62 kB |
| `packages/icon/dist/XWithCircle.js` | 1.56 kB |
| `packages/info-sprinkle/dist/esm/index.js` | 1.83 kB |
| `packages/info-sprinkle/dist/index.js` | 2.02 kB |
| `packages/inline-definition/dist/esm/index.js` | 1.69 kB |
| `packages/inline-definition/dist/index.js` | 1.85 kB |
| `packages/input-option/dist/esm/index.js` | 2.67 kB |
| `packages/input-option/dist/index.js` | 2.81 kB |
| `packages/leafygreen-provider/dist/esm/index.js` | 1.86 kB |
| `packages/leafygreen-provider/dist/index.js` | 2.02 kB |
| `packages/lib/dist/esm/index.js` | 2.54 kB |
| `packages/lib/dist/index.js` | 2.7 kB |
| `packages/loading-indicator/dist/esm/index.js` | 3.38 kB |
| `packages/loading-indicator/dist/index.js` | 3.55 kB |
| `packages/logo/dist/esm/index.js` | 39 kB |
| `packages/logo/dist/index.js` | 39.3 kB |
| `packages/marketing-modal/dist/esm/index.js` | 3.49 kB |
| `packages/marketing-modal/dist/index.js` | 3.69 kB |
| `packages/menu/dist/esm/index.js` | 7.94 kB |
| `packages/menu/dist/index.js` | 8.18 kB |
| `packages/modal/dist/esm/index.js` | 3.4 kB |
| `packages/modal/dist/index.js` | 3.61 kB |
| `packages/number-input/dist/esm/index.js` | 5.64 kB |
| `packages/number-input/dist/index.js` | 5.82 kB |
| `packages/ordered-list/dist/esm/index.js` | 1.32 kB |
| `packages/ordered-list/dist/index.js` | 1.49 kB |
| `packages/pagination/dist/esm/index.js` | 1.68 kB |
| `packages/pagination/dist/index.js` | 1.88 kB |
| `packages/palette/dist/esm/index.js` | 411 B |
| `packages/palette/dist/index.js` | 562 B |
| `packages/password-input/dist/esm/index.js` | 4.42 kB |
| `packages/password-input/dist/index.js` | 4.64 kB |
| `packages/pipeline/dist/esm/index.js` | 7.85 kB |
| `packages/pipeline/dist/index.js` | 8.1 kB |
| `packages/polymorphic/dist/esm/index.js` | 1.27 kB |
| `packages/polymorphic/dist/index.js` | 1.4 kB |
| `packages/popover/dist/esm/index.js` | 4.98 kB |
| `packages/popover/dist/index.js` | 5.14 kB |
| `packages/portal/dist/esm/index.js` | 929 B |
| `packages/portal/dist/index.js` | 1.09 kB |
| `packages/radio-box-group/dist/esm/index.js` | 2.87 kB |
| `packages/radio-box-group/dist/index.js` | 2.99 kB |
| `packages/radio-group/dist/esm/index.js` | 3.25 kB |
| `packages/radio-group/dist/index.js` | 3.38 kB |
| `packages/ripple/dist/esm/index.js` | 956 B |
| `packages/ripple/dist/index.js` | 1.05 kB |
| `packages/search-input/dist/esm/index.js` | 5.89 kB |
| `packages/search-input/dist/index.js` | 6.1 kB |
| `packages/segmented-control/dist/esm/index.js` | 5.44 kB |
| `packages/segmented-control/dist/index.js` | 5.57 kB |
| `packages/select/dist/esm/index.js` | 8.9 kB |
| `packages/select/dist/index.js` | 9.11 kB |
| `packages/side-nav/dist/esm/index.js` | 7.46 kB |
| `packages/side-nav/dist/index.js` | 7.7 kB |
| `packages/skeleton-loader/dist/esm/index.js` | 3.19 kB |
| `packages/skeleton-loader/dist/index.js` | 3.39 kB |
| `packages/split-button/dist/esm/index.js` | 3.46 kB |
| `packages/split-button/dist/index.js` | 3.63 kB |
| `packages/stepper/dist/esm/index.js` | 3.47 kB |
| `packages/stepper/dist/index.js` | 3.67 kB |
| `packages/table/dist/esm/index.js` | 8.3 kB |
| `packages/table/dist/index.js` | 8.55 kB |
| `packages/tabs/dist/esm/index.js` | 5.42 kB |
| `packages/tabs/dist/index.js` | 5.56 kB |
| `packages/testing-lib/dist/esm/index.js` | 4.52 kB |
| `packages/testing-lib/dist/index.js` | 4.74 kB |
| `packages/testing-lib/dist/renderHookServer.js` | 391 B |
| `packages/testing-lib/dist/renderHookServerV17.js` | 239 B |
| `packages/text-area/dist/esm/index.js` | 2.38 kB |
| `packages/text-area/dist/index.js` | 2.54 kB |
| `packages/text-input/dist/esm/index.js` | 2.7 kB |
| `packages/text-input/dist/index.js` | 2.86 kB |
| `packages/toast/dist/esm/index.js` | 8.22 kB |
| `packages/toast/dist/index.js` | 8.49 kB |
| `packages/toggle/dist/esm/index.js` | 3.05 kB |
| `packages/toggle/dist/index.js` | 3.23 kB |
| `packages/tokens/dist/esm/index.js` | 2.5 kB |
| `packages/tokens/dist/index.js` | 2.62 kB |
| `packages/tooltip/dist/esm/index.js` | 4.3 kB |
| `packages/tooltip/dist/index.js` | 4.58 kB |
| `packages/typography/dist/esm/index.js` | 5.64 kB |
| `packages/typography/dist/index.js` | 5.8 kB |
| `tools/build/dist/esm/index.js` | 5.95 kB |
| `tools/build/dist/index.js` | 6.18 kB |
| `tools/cli/dist/esm/index.js` | 2.19 kB |
| `tools/cli/dist/index.js` | 2.35 kB |
| `tools/codemods/dist/_virtual/_rollupPluginBabelHelpers.js` | 940 B |
| `tools/codemods/dist/codemods/popover-v12/transform.js` | 1.11 kB |
| `tools/codemods/dist/constants.js` | 148 B |
| `tools/codemods/dist/esm/_virtual/_rollupPluginBabelHelpers.mjs` | 935 B |
| `tools/codemods/dist/esm/codemods/popover-v12/transform.mjs` | 1.09 kB |
| `tools/codemods/dist/esm/constants.mjs` | 148 B |
| `tools/codemods/dist/esm/index.js` | 4.58 kB |
| `tools/codemods/dist/esm/types.mjs` | 765 B |
| `tools/codemods/dist/esm/utils/imports/getImportSpecifiersForDeclaration.mjs` | 280 B |
| `tools/codemods/dist/esm/utils/jsx/getJSXAttributes.mjs` | 161 B |
| `tools/codemods/dist/esm/utils/jsx/insertJSXComment/insertJSXComment.mjs` | 373 B |
| `tools/codemods/dist/esm/utils/transformations/addJSXAttributes/addJSXAttributes.mjs` | 414 B |
| `tools/codemods/dist/esm/utils/transformations/consolidateJSXAttributes/consolidateJSXAttributes.mjs` | 666 B |
| `tools/codemods/dist/esm/utils/transformations/removeJSXAttributes/removeJSXAttributes.mjs` | 362 B |
| `tools/codemods/dist/esm/utils/transformations/replaceJSXAttributes/replaceJSXAttributes.mjs` | 280 B |
| `tools/codemods/dist/index.js` | 4.8 kB |
| `tools/codemods/dist/types.js` | 765 B |
| `tools/codemods/dist/utils/imports/getImportSpecifiersForDeclaration.js` | 283 B |
| `tools/codemods/dist/utils/jsx/getJSXAttributes.js` | 165 B |
| `tools/codemods/dist/utils/jsx/insertJSXComment/insertJSXComment.js` | 373 B |
| `tools/codemods/dist/utils/transformations/addJSXAttributes/addJSXAttributes.js` | 412 B |
| `tools/codemods/dist/utils/transformations/consolidateJSXAttributes/consolidateJSXAttributes.js` | 664 B |
| `tools/codemods/dist/utils/transformations/removeJSXAttributes/removeJSXAttributes.js` | 366 B |
| `tools/codemods/dist/utils/transformations/replaceJSXAttributes/replaceJSXAttributes.js` | 283 B |
| `tools/create/dist/esm/index.js` | 3.6 kB |
| `tools/create/dist/index.js` | 3.76 kB |
| `tools/install/dist/esm/index.js` | 2.14 kB |
| `tools/install/dist/index.js` | 2.29 kB |
| `tools/link/dist/esm/index.js` | 5.04 kB |
| `tools/link/dist/index.js` | 5.15 kB |
| `tools/lint/dist/esm/index.js` | 5.33 kB |
| `tools/lint/dist/index.js` | 5.47 kB |
| `tools/meta/dist/esm/index.js` | 1.68 kB |
| `tools/meta/dist/index.js` | 1.84 kB |
| `tools/slackbot/dist/esm/index.js` | 6.1 kB |
| `tools/slackbot/dist/index.js` | 6.27 kB |
| `tools/storybook-addon/dist/esm/index.js` | 2.24 kB |
| `tools/storybook-addon/dist/esm/main.js` | 2.2 kB |
| `tools/storybook-addon/dist/esm/manager.js` | 554 B |
| `tools/storybook-addon/dist/esm/preview.js` | 1.6 kB |
| `tools/storybook-addon/dist/index.js` | 2.36 kB |
| `tools/storybook-addon/dist/main.js` | 2.31 kB |
| `tools/storybook-addon/dist/manager.js` | 686 B |
| `tools/storybook-addon/dist/preview.js` | 1.69 kB |
| `tools/storybook-decorators/dist/esm/index.js` | 3.29 kB |
| `tools/storybook-decorators/dist/index.js` | 3.56 kB |
| `tools/storybook-utils/dist/esm/index.js` | 4.01 kB |
| `tools/storybook-utils/dist/index.js` | 4.13 kB |
| `tools/test-harnesses/dist/esm/index.js` | 1.79 kB |
| `tools/test-harnesses/dist/index.js` | 1.92 kB |
| `tools/test/dist/esm/index.js` | 1.69 kB |
| `tools/test/dist/index.js` | 1.82 kB |
| `tools/update/dist/esm/index.js` | 730 B |
| `tools/update/dist/index.js` | 896 B |
| `tools/validate/dist/esm/index.js` | 6.96 kB |
| `tools/validate/dist/index.js` | 7.19 kB |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR