Skip to content

feat: implement auto-discovery system for playground components - #306

Merged
aagarwal1012 merged 13 commits into
dodopayments:mainfrom
yashranaway:playground-auto-discovery
Nov 7, 2025
Merged

aagarwal1012 merged 13 commits into
dodopayments:mainfrom
yashranaway:playground-auto-discovery

Conversation

@yashranaway

@yashranaway yashranaway commented Oct 16, 2025 •

Copy link
Copy Markdown
Contributor

Summary

Implement a zero-maintenance playground component loader by replacing the manual registry with an auto-discovery system that scans the public registry JSON and loads components dynamically.

Changes

  • Auto-discovery core: Added src/components/playground/auto-discovery.ts to fetch registry metadata, import components on demand, and infer usage snippets.
  • Playground integration: Updated src/components/playground/playground-header.tsx and src/components/playground/playground.tsx to rely on the discovery utility for the selector and deep-link handling.
  • Docs: Added AUTO_DISCOVERY.md to document architecture, benefits, setup, and troubleshooting.

Screenshots/Recordings (if UI)

No UI changes requiring screenshots.

How to Test

  1. npm ci
  2. npm run typecheck
  3. npm run build

Checklist

  • Title is clear and descriptive
  • Related issue linked (if any)
  • Tests or manual verification steps included
  • CI passes (typecheck & build)
  • Docs updated (if needed)

Summary by CodeRabbit

  • New Features

    • Playground now auto-discovers components, loads them on demand with loading/error states, supports slug-based deep links, and added a "Payment" category.
  • Bug Fixes

    • Removed automatic first-component selection on mount; selection is now explicit and user-driven.
    • Proration demo confirmations/cancels are now no-ops; demo tabs are horizontally scrollable.
  • Chores

    • Sitemap rebuilt with many refreshed URLs and updated last-mod timestamps.

@vercel

vercel Bot commented Oct 16, 2025

Copy link
Copy Markdown

@yashranaway is attempting to deploy a commit to the Dodo Payments Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Oct 16, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a manifest-driven auto-discovery system for playground components, integrates it into playground-header and playground core to load components dynamically, removes the local component registry and the usePlaygroundState hook/export, updates public/sitemap-0.xml, and adjusts .gitignore for sitemap/robots files.

Changes

Cohort / File(s) Summary
Sitemap Update
public/sitemap-0.xml
Appended many sitemap URL entries (playground, docs, llms, docs/components paths) with updated lastmod timestamps; no deletions.
Gitignore
.gitignore
Added ignore rules for sitemap.xml and public/robots.txt (removed prior negation), extended sitemap ignores.
Auto-Discovery Module (new)
src/components/playground/auto-discovery.ts
New manifest-driven discovery: fetch registry manifest and per-component metadata, normalize names, categorize, extract safe usage snippets, dynamically import components (resolve multiple export patterns), build ComponentConfig, and export discoverComponents(), discoverComponent(), getComponentNames(), getComponentList() and ComponentListItem.
Playground Header
src/components/playground/playground-header.tsx
Load lightweight list via getComponentList() on mount, add isLoading/isLoadingComponent/loadError states, disable/adjust selector while loading, use discoverComponent() for selection, add "payment" category, and replace registry usage with discovered list.
Playground Core
src/components/playground/playground.tsx
Replace local registry/dynamic-import fallback with slug-driven discoverComponent(slug) selection flow; removed prior dynamic import and usage-snippet fetch logic; retained UI/resize behavior.
Removed Local Registry
src/components/playground/component-registry.tsx
Deleted full componentRegistry export and all static component entries and imports.
Removed Hook & Index Export
src/components/playground/use-playground-state.ts, src/components/playground/index.ts
Removed usePlaygroundState hook file and removed re-export of usePlaygroundState from the module index.
Proration Demo Tweaks
src/components/proration-preview-demo.tsx, src/registry/billingsdk/demo/proration-preview-demo.tsx
Replaced all onConfirm/onCancel callback bodies with no-op functions in demos; no other prop shapes changed.

Sequence Diagram(s)

sequenceDiagram
    participant PH as PlaygroundHeader
    participant PC as PlaygroundMain
    participant AD as AutoDiscovery
    participant Registry as Public /r/ (manifest)
    participant SDK as billingsdk/

    rect rgba(0,128,128,0.06)
    PH->>AD: getComponentList() (on mount)
    AD->>Registry: GET /r/registry.json
    Registry-->>AD: manifest
    AD->>Registry: GET /r/{name}.json (parallel)
    Registry-->>AD: metadata
    AD->>SDK: dynamic import billingsdk/...
    SDK-->>AD: component module
    AD-->>PH: ComponentListItem[] (lightweight)
    end

    rect rgba(128,64,0,0.06)
    PC->>AD: discoverComponent(slug)
    AD->>Registry: GET /r/{slug}.json
    Registry-->>AD: metadata
    AD->>SDK: dynamic import billingsdk/... (resolve main export)
    SDK-->>AD: component module
    AD-->>PC: ComponentConfig | null
    PC->>PC: setSelectedComponent(config)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Potential attention points:

  • auto-discovery.ts dynamic import resolution, timeout/error/reDoS protections around snippet extraction.
  • Integration points in playground-header.tsx and playground.tsx replacing registry logic.
  • Any residual references to the removed component-registry or usePlaygroundState.

Possibly related PRs

  • registry build #245 — Registry JSON updates that the new auto-discovery consumes (adds/edits registry entries referenced by discovery).
  • feat: Usage based pricing Slider #184 — Changes to the old component registry that may conflict with the registry removal in this PR.
  • UI Revamp #282 — Overlapping edits to the playground registry and sitemap updates related to discovery.

Suggested reviewers

  • tsahil01

Poem

🐇
I hopped the manifest trail and fetched each name with glee,
Pulled exports from billingsdk and set the playground free.
Headers hum, components bloom, selection finds its light—
A rabbit read the registry and hopped the demos bright. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: implementing an auto-discovery system for playground components, which aligns with the core objective of replacing manual registry with dynamic component loading.
Description check ✅ Passed The description covers all required template sections: Summary explains the purpose, Changes lists key modifications, How to Test provides build steps, and Checklist indicates docs were updated; only unrelated sections (screenshots, issue link, tests) are appropriately omitted or noted as N/A.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fbd312 and f13b3a0.

📒 Files selected for processing (1)
  • src/components/proration-preview-demo.tsx (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/proration-preview-demo.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Typecheck and Build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/components/playground/auto-discovery.ts (3)

86-96: Harden and simplify snippet extraction regex; avoid variable-pattern pitfalls.

Current pattern is vulnerable to pathological backtracking and can miss self-closing tags. Escape the name and use a single alternation that handles paired/self‑closing tags.

-      const componentPattern = new RegExp(`<${pascalName}[^>]*(?:>|\\/>)[\\s\\S]*?(?:<\\/${pascalName}>|(?=\\n\\s*\\)))`, 'i');
+      const escaped = pascalName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+      const componentPattern = new RegExp(
+        `<${escaped}\\b[^>]*>([\\s\\S]*?)<\\/${escaped}>|<${escaped}\\b[^>]*/>`,
+        'i'
+      );

Optionally, apply the same approach to the fallback regex. Based on static analysis hints.


112-121: Add camelCase export fallback; improve resilience.

Many modules export camelCase names. Try that before giving up.

     const module = await import(`@/components/billingsdk/${componentName}`);
     const pascalName = toPascalCase(componentName);
+    const camelName = componentName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());

     // Try multiple export patterns
-    const component = module[pascalName] || module.default || module[componentName];
+    const component =
+      module[pascalName] ||
+      module.default ||
+      module[camelName] ||
+      module[componentName];

27-36: Add fetch timeouts to avoid hanging UI.

Wrap fetch with AbortController to fail fast.

 async function fetchRegistryManifest(): Promise<RegistryManifest | null> {
   try {
-    const response = await fetch('/r/registry.json');
+    const ac = new AbortController();
+    const t = setTimeout(() => ac.abort(), 10000);
+    const response = await fetch('/r/registry.json', { signal: ac.signal });
+    clearTimeout(t);
     if (!response.ok) return null;
     return await response.json();
   } catch (error) {
 async function fetchComponentMetadata(componentName: string): Promise<any> {
   try {
-    const response = await fetch(`/r/${componentName}.json`);
+    const ac = new AbortController();
+    const t = setTimeout(() => ac.abort(), 10000);
+    const response = await fetch(`/r/${componentName}.json`, { signal: ac.signal });
+    clearTimeout(t);
     if (!response.ok) return null;
     return await response.json();
   } catch (error) {

Also applies to: 41-50

src/components/playground/playground-header.tsx (2)

104-108: Use SelectValue without children.

shadcn/ui SelectValue is self-closing; children are ignored. Rely on placeholder and selected item label.

-            <SelectTrigger className="w-64">
-              <SelectValue placeholder={isLoading ? "Loading components..." : "Select a component"}>
-                {state.selectedComponent?.name || (isLoading ? "Loading..." : "Select a component")}
-              </SelectValue>
+            <SelectTrigger className="w-64">
+              <SelectValue placeholder={isLoading ? "Loading components..." : "Select a component"} />

40-43: Minor: remove redundant filtering state.

selectedCategory is fixed to "all"; filteredComponents mirrors components. Simplify by using components directly (or implement category selection).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf83012 and 72f0134.

📒 Files selected for processing (4)
  • public/sitemap-0.xml (1 hunks)
  • src/components/playground/auto-discovery.ts (1 hunks)
  • src/components/playground/playground-header.tsx (2 hunks)
  • src/components/playground/playground.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/playground/playground.tsx (1)
src/components/playground/auto-discovery.ts (1)
  • discoverComponent (202-241)
src/components/playground/playground-header.tsx (3)
src/components/playground/playground-context.tsx (1)
  • usePlayground (257-263)
src/components/playground/types.ts (1)
  • ComponentConfig (1-10)
src/components/playground/auto-discovery.ts (1)
  • discoverComponents (132-196)
🪛 ast-grep (0.39.6)
src/components/playground/auto-discovery.ts

[warning] 86-86: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(<${pascalName}[^>]*(?:>|\\/>)[\\s\\S]*?(?:<\\/${pascalName}>|(?=\\n\\s*\\))), 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Typecheck and Build
🔇 Additional comments (1)
src/components/playground/playground.tsx (1)

55-69: Slug parsing + discovery hookup looks good.

Decoding, normalization, and guarded selection are correct.

@yashranaway yashranaway changed the title feat: implement smart auto-discovery system for playground components feat: implement auto-discovery system for playground components Oct 16, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/components/playground/playground-header.tsx (1)

21-30: Lazy list + on-demand load looks good; resolves earlier eager-import concern.

Switched to getComponentList() on mount and discoverComponent() on selection. This addresses the prior perf warning in the header.

Also applies to: 44-59

🧹 Nitpick comments (8)
src/components/playground/playground-header.tsx (4)

16-16: Remove unused category state and simplify filtering.

selectedCategory is fixed to "all" and never updated; filteredComponents is thus always componentList. Simplify and avoid dead state.

Apply:

-  const [selectedCategory] = useState<string>("all");
-  const filteredComponents = selectedCategory === "all" 
-    ? componentList 
-    : componentList.filter(comp => comp.category === selectedCategory);
+  // Category filtering not yet implemented; use full list.
+  const filteredComponents = componentList;

Also applies to: 40-43, 132-134


126-126: Tailwind class likely invalid: use min-w-[16rem] instead of min-w-64.

Tailwind provides w-64 but not min-w-64. Use an arbitrary value class.

-            <SelectContent className="w-64 min-w-64 max-h-96 overflow-y-auto">
+            <SelectContent className="w-64 min-w-[16rem] max-h-96 overflow-y-auto">

22-30: Guard setState on unmount.

Add AbortController or a mounted flag to avoid state updates after unmount.

   useEffect(() => {
-    async function loadComponentList() {
+    let cancelled = false;
+    async function loadComponentList() {
       setIsLoading(true);
       const list = await getComponentList();
-      setComponentList(list);
-      setIsLoading(false);
+      if (!cancelled) {
+        setComponentList(list);
+        setIsLoading(false);
+      }
     }
     loadComponentList();
-  }, []);
+    return () => {
+      cancelled = true;
+    };
+  }, []);

120-124: SelectValue children are unnecessary with shadcn/ui.

SelectValue typically uses the placeholder prop and renders selected text automatically; custom children here likely won’t render. Keep placeholder logic; drop children to reduce noise.

-              <SelectValue placeholder={isLoading ? "Loading components..." : "Select a component"}>
-                {isLoadingComponent 
-                  ? "Loading component..." 
-                  : state.selectedComponent?.name || (isLoading ? "Loading..." : "Select a component")}
-              </SelectValue>
+              <SelectValue placeholder={isLoading ? "Loading components..." : (isLoadingComponent ? "Loading component..." : "Select a component")} />
src/components/playground/auto-discovery.ts (4)

202-218: Improve name normalization for acronyms and number↔letter boundaries.

Handle “APICard” → “api-card” and “v2Plan”/“plan2X” cases.

-function normalizeComponentName(name: string): string {
-  return name
-    // Insert hyphens between lower→upper transitions (decamelize)
-    .replace(/([a-z])([A-Z])/g, '$1-$2')
-    // Insert hyphens between letter→number transitions
-    .replace(/([a-zA-Z])([0-9])/g, '$1-$2')
-    // Lowercase everything
-    .toLowerCase()
-    // Replace whitespace with hyphens
-    .replace(/\s+/g, '-')
-    // Remove non a-z0-9- characters
-    .replace(/[^a-z0-9-]/g, '')
-    // Collapse multiple hyphens
-    .replace(/-+/g, '-')
-    // Trim leading/trailing hyphens
-    .replace(/^-+|-+$/g, '');
-}
+function normalizeComponentName(name: string): string {
+  return name
+    .trim()
+    // lower/number -> Upper (e.g., "planId" → "plan-Id")
+    .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
+    // ACRONYM -> Word (e.g., "APIClient" → "API-Client")
+    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
+    // number -> letter and letter -> number
+    .replace(/([0-9])([a-zA-Z])/g, '$1-$2')
+    .replace(/([a-zA-Z])([0-9])/g, '$1-$2')
+    // spaces/underscores -> hyphens
+    .replace(/[\s_]+/g, '-')
+    // strip invalid
+    .replace(/[^a-zA-Z0-9-]/g, '')
+    // collapse hyphens
+    .replace(/-+/g, '-')
+    // trim hyphens
+    .replace(/^-+|-+$/g, '')
+    .toLowerCase();
+}

151-183: Avoid unbounded parallel fetch/imports; limit concurrency.

Large registries can spike network/CPU and bloat memory. Process in small batches.

-  // Process each component in parallel for better performance
-  const results = await Promise.allSettled(
-    componentItems.map(async (item) => {
-      try {
-        // Fetch detailed metadata
-        const metadata = await fetchComponentMetadata(item.name);
-        
-        // Dynamically import the component
-        const Component = await importComponent(item.name);
-        
-        if (!Component) {
-          console.warn(`Skipping ${item.name}: Component not found`);
-          return null;
-        }
-
-        const config: ComponentConfig = {
-          id: item.name,
-          name: item.title,
-          description: item.description,
-          category: categorizeComponent(item.name),
-          component: Component,
-          imports: [`@/components/billingsdk/${item.name}`],
-          defaultCode: extractUsageSnippet(metadata, item.name),
-          defaultProps: {},
-        };
-
-        return config;
-      } catch (error) {
-        console.error(`Failed to process component ${item.name}:`, error);
-        return null;
-      }
-    })
-  );
+  const concurrency = 6;
+  const results: PromiseSettledResult<ComponentConfig | null>[] = [];
+  for (let i = 0; i < componentItems.length; i += concurrency) {
+    const slice = componentItems.slice(i, i + concurrency);
+    // process a batch
+    const batch = await Promise.allSettled(
+      slice.map(async (item) => {
+        try {
+          const metadata = await fetchComponentMetadata(item.name);
+          const Component = await importComponent(item.name);
+          if (!Component) {
+            console.warn(`Skipping ${item.name}: Component not found`);
+            return null;
+          }
+          return {
+            id: item.name,
+            name: item.title,
+            description: item.description,
+            category: categorizeComponent(item.name),
+            component: Component,
+            imports: [`@/components/billingsdk/${item.name}`],
+            defaultCode: extractUsageSnippet(metadata, item.name),
+            defaultProps: {},
+          } as ComponentConfig;
+        } catch (error) {
+          console.error(`Failed to process component ${item.name}:`, error);
+          return null;
+        }
+      })
+    );
+    results.push(...batch);
+  }

27-36: Memoize registry/metadata to reduce redundant fetches.

getComponentList/getComponentNames/discovery all hit the manifest; add module-level caching to cut network calls on the same page load.

+let manifestCache: Promise<RegistryManifest | null> | null = null;
 async function fetchRegistryManifest(): Promise<RegistryManifest | null> {
-  try {
+  try {
-    const response = await fetch('/r/registry.json');
+    if (!manifestCache) {
+      manifestCache = (async () => {
+        const response = await fetch('/r/registry.json');
+        if (!response.ok) return null;
+        return await response.json();
+      })();
+    }
-    if (!response.ok) return null;
-    return await response.json();
+    return await manifestCache;
   } catch (error) {
     console.error('Failed to fetch registry manifest:', error);
     return null;
   }
 }

Optionally cache per-component metadata with a Map<string, Promise>.

Also applies to: 276-296, 301-313


149-150: Reduce console noise in production.

Wrap logs behind a debug flag or use a logger with levels to avoid noisy consoles in prod.

Also applies to: 194-196, 229-231, 254-255

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72f0134 and 4ecd4b8.

📒 Files selected for processing (2)
  • src/components/playground/auto-discovery.ts (1 hunks)
  • src/components/playground/playground-header.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/playground/playground-header.tsx (2)
src/components/playground/playground-context.tsx (1)
  • usePlayground (257-263)
src/components/playground/auto-discovery.ts (3)
  • ComponentListItem (265-270)
  • getComponentList (276-296)
  • discoverComponent (224-260)
🪛 ast-grep (0.39.6)
src/components/playground/auto-discovery.ts

[warning] 86-86: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(<${pascalName}[^>]*(?:>|\\/>)[\\s\\S]*?(?:<\\/${pascalName}>|(?=\\n\\s*\\))), 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Typecheck and Build

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/playground/auto-discovery.ts (1)

156-220: Note: Eager import function still exported.

The discoverComponents() function imports all components up front (lines 175-183), which past reviews flagged as bloating initial load. While getComponentList() now provides a lightweight alternative, this function remains exported.

If discoverComponents() is no longer needed for the primary use case, consider:

  1. Deprecating it with a JSDoc @deprecated tag directing users to getComponentList() + discoverComponent()
  2. Documenting when it should be used (e.g., bulk pre-loading scenarios)
  3. Making it internal (not exported) if there's no valid use case

Would you like me to generate a deprecation notice or alternative approach?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ecd4b8 and 5894d58.

📒 Files selected for processing (1)
  • src/components/playground/auto-discovery.ts (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.39.6)
src/components/playground/auto-discovery.ts

[warning] 102-105: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
<${escapedName}[^>]{0,500}(?:>|\\/>)(?:[^<]{0,2000}|(?:<(?!\\/${escapedName}>)[^>]{0,100}>){0,50})*?(?:<\\/${escapedName}>|(?=\\n\\s*\\))),
'i'
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Typecheck and Build
🔇 Additional comments (6)
src/components/playground/auto-discovery.ts (6)

1-78: LGTM! Solid foundation with proper error handling.

The type definitions, fetch utilities, and helper functions are well-structured. Error handling is appropriate with try-catch blocks and null returns. The regexEscape function properly addresses injection concerns.


84-129: Regex ReDoS mitigations in place; static analysis warning acknowledged.

The implementation includes multiple defenses against ReDoS attacks:

  • Input size capped at 50KB (line 95)
  • Variable escaped with regexEscape (line 99)
  • Bounded quantifiers throughout (lines 103-106)

Static analysis tools still flag the RegExp constructor pattern, but the mitigations significantly reduce the risk. The bounded quantifiers ({0,500}, {0,2000}, etc.) prevent catastrophic backtracking.

Based on static analysis hints


134-151: Verify registry integrity and consider path validation.

The dynamic import on line 136 constructs paths from registry data (componentName). While the path is scoped to @/components/billingsdk/, if the registry JSON is compromised, it could reference arbitrary modules within that directory.

Consider adding explicit path validation to ensure componentName contains only expected characters:

 async function importComponent(componentName: string): Promise<any> {
   try {
+    // Validate component name format (kebab-case only)
+    if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(componentName)) {
+      console.error(`Invalid component name format: ${componentName}`);
+      return null;
+    }
+    
     const module = await import(`@/components/billingsdk/${componentName}`);

This ensures only properly formatted kebab-case component names can be imported, reducing the attack surface if the registry is compromised.


226-242: LGTM! Proper PascalCase → kebab-case normalization.

The function correctly handles the case conversion issue flagged in previous reviews. It properly decamelizes PascalCase/camelCase with transition detection, normalizes spacing, and cleans up edge cases.

Based on past review comments (now resolved)


248-284: LGTM! Clean single-component discovery.

The function properly normalizes component names, handles import failures gracefully, and provides useful logging. Error handling is appropriate with try-catch and null returns.


289-337: LGTM! Lightweight API addresses eager-loading concern.

The getComponentList() function and ComponentListItem interface provide metadata-only access without importing actual components, effectively solving the bundle bloat issue raised in past reviews. This is the correct pattern for initial loading.

Based on past review comments (now resolved)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/components/playground/auto-discovery.ts (5)

134-151: Dynamic import path creates a large context chunk; prefer explicit module map.

import(\@/components/billingsdk/${componentName}`)` makes bundlers include the entire directory in a single context chunk, undermining code‑splitting and bloating first load.

  • If using Vite: use import.meta.glob to build a map and load only the matched module.
  • If using Next/Webpack: generate a loader map at build time (from the registry) or a narrow require.context.

Please confirm bundler. I can provide a concrete patch (Vite glob or Next/Webpack map) once confirmed. Example (Vite):

// top-level
const modules = import.meta.glob('@/components/billingsdk/**/*.{tsx,jsx,ts,js}', { eager: false });

async function importComponent(componentName: string) {
  const entry = Object.keys(modules).find(
    (k) => k.endsWith(`/${componentName}.tsx`) || k.endsWith(`/${componentName}/index.tsx`)
  );
  return entry ? (await modules[entry]())?.default ?? (await modules[entry]())[toPascalCase(componentName)] : null;
}

226-242: Strengthen name normalization (handle acronyms and digit boundaries).

Current regex misses cases like “UIForm”, “XMLHttp”, and number→letter. Use a more robust pipeline.

Apply:

-function normalizeComponentName(name: string): string {
-  return name
-    // Insert hyphens between lower→upper transitions (decamelize)
-    .replace(/([a-z])([A-Z])/g, '$1-$2')
-    // Insert hyphens between letter→number transitions
-    .replace(/([a-zA-Z])([0-9])/g, '$1-$2')
-    // Lowercase everything
-    .toLowerCase()
-    // Replace whitespace with hyphens
-    .replace(/\s+/g, '-')
-    // Remove non a-z0-9- characters
-    .replace(/[^a-z0-9-]/g, '')
-    // Collapse multiple hyphens
-    .replace(/-+/g, '-')
-    // Trim leading/trailing hyphens
-    .replace(/^-+|-+$/g, '');
-}
+function normalizeComponentName(name: string): string {
+  return name
+    .trim()
+    .replace(/[_\s]+/g, '-')
+    .replace(/([a-z0-9])([A-Z])/g, '$1-$2')        // lower/digit → Upper
+    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')     // ACRONYM→Capitalized
+    .replace(/([a-zA-Z])([0-9])/g, '$1-$2')        // letter → digit
+    .replace(/([0-9])([a-zA-Z])/g, '$1-$2')        // digit → letter
+    .toLowerCase()
+    .replace(/[^a-z0-9-]/g, '')
+    .replace(/-+/g, '-')
+    .replace(/^-+|-+$/g, '');
+}

27-36: Add timeout and lightweight cache for fetches; consider SSR-safe URLs.

Avoid hanging fetches and duplicate manifest downloads; also ensure SSR calls don’t use relative “/r/...”.

Apply caching + timeout to manifest; replicate to metadata as needed:

+let manifestCache: { data: RegistryManifest | null; ts: number } = { data: null, ts: 0 };
+const MANIFEST_TTL_MS = 60_000;
+
 async function fetchRegistryManifest(): Promise<RegistryManifest | null> {
-  try {
-    const response = await fetch('/r/registry.json');
+  const now = Date.now();
+  if (manifestCache.data && now - manifestCache.ts < MANIFEST_TTL_MS) return manifestCache.data;
+  try {
+    const controller = new AbortController();
+    const timer = setTimeout(() => controller.abort(), 8000);
+    // If SSR, prefer absolute base (e.g., NEXT_PUBLIC_BASE_URL); keep relative for CSR.
+    const url = typeof window === 'undefined' && process.env.NEXT_PUBLIC_BASE_URL
+      ? new URL('/r/registry.json', process.env.NEXT_PUBLIC_BASE_URL).toString()
+      : '/r/registry.json';
+    const response = await fetch(url, { signal: controller.signal } as RequestInit);
+    clearTimeout(timer);
     if (!response.ok) return null;
-    return await response.json();
+    const data = await response.json();
+    manifestCache = { data, ts: now };
+    return data;
   } catch (error) {
     console.error('Failed to fetch registry manifest:', error);
     return null;
   }
 }

And for metadata:

 async function fetchComponentMetadata(componentName: string): Promise<any> {
   try {
-    const response = await fetch(`/r/${componentName}.json`);
+    const controller = new AbortController();
+    const timer = setTimeout(() => controller.abort(), 8000);
+    const url = typeof window === 'undefined' && process.env.NEXT_PUBLIC_BASE_URL
+      ? new URL(`/r/${componentName}.json`, process.env.NEXT_PUBLIC_BASE_URL).toString()
+      : `/r/${componentName}.json`;
+    const response = await fetch(url, { signal: controller.signal } as RequestInit);
+    clearTimeout(timer);
     if (!response.ok) return null;
     return await response.json();
   } catch (error) {

Confirm runtime (Next vs Vite) so we can align the base URL approach.

Also applies to: 41-50


314-320: Return lists in stable, sorted order.

Sorting improves UX and determinism.

Apply:

   return componentItems.map(item => ({
     id: item.name,
     name: item.title,
     description: item.description,
     category: categorizeComponent(item.name),
-  }));
+  })).sort((a, b) => a.name.localeCompare(b.name));
   return manifest.items
     .filter(item => 
       item.type === 'registry:block' && 
       item.name !== 'index' && 
       item.name !== 'all' &&
       item.name !== 'hello-world'
     )
-    .map(item => item.name);
+    .map(item => item.name)
+    .sort((a, b) => a.localeCompare(b));

Also applies to: 329-337


173-174: Gate console logs behind a debug flag.

Avoid noisy logs in production; keep them when developing.

Example:

const DEBUG = process.env.NODE_ENV !== 'production';
// then: if (DEBUG) console.log(...)

Also applies to: 218-219, 253-254, 278-279

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5894d58 and 61d978f.

📒 Files selected for processing (1)
  • src/components/playground/auto-discovery.ts (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.39.6)
src/components/playground/auto-discovery.ts

[warning] 102-105: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
<${escapedName}[^>]{0,500}(?:>|\\/>)(?:[^<]{0,2000}|(?:<(?!\\/${escapedName}>)[^>]{0,100}>){0,50})*?(?:<\\/${escapedName}>|(?=\\n\\s*\\))),
'i'
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Typecheck and Build
🔇 Additional comments (3)
src/components/playground/auto-discovery.ts (3)

114-117: Regex escaping fixed correctly.

The fallback pattern’s escapes are now correct (single-escaped in string literal). Resolves the prior critical issue.


101-107: Regex hardening looks good.

Variable is escaped, input is capped (50KB), and bounded quantifiers reduce backtracking. This addresses the ReDoS concerns.

Based on static analysis hints


156-220: Ensure this batch importer isn’t used on initial render paths.

This imports all components and fetches all metadata in parallel; great for prefetch/dev, heavy for initial page load.

Please confirm header/playground only call getComponentList() initially and discoverComponent() on selection (not discoverComponents()).

yashranaway and others added 4 commits October 16, 2025 17:12
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@tsahil01

Copy link
Copy Markdown
Member

@yashranaway can we need to get rid of component-registry.tsx? Its still been used in use-playground-state.ts

@yashranaway

Copy link
Copy Markdown
Contributor Author

@yashranaway can we need to get rid of component-registry.tsx? Its still been used in use-playground-state.ts

sure

@yashranaway

Copy link
Copy Markdown
Contributor Author

@tsahil01 lmk if anything else needs updating, major cleaning lol
image

@tsahil01

Copy link
Copy Markdown
Member

I just noticed, some of the components are not rendering. Its stuck on Loading component...
Screenshot 2025-10-20 at 10 46 32 AM

@yashranaway

Copy link
Copy Markdown
Contributor Author

I will check and fix this

@yashranaway

Copy link
Copy Markdown
Contributor Author

I just noticed, some of the components are not rendering. Its stuck on Loading component...
Screenshot 2025-10-20 at 10 46 32 AM

Which component was this ?

@yashranaway

Copy link
Copy Markdown
Contributor Author

@tsahil01 it is now fixed, some components when first time loaded take around ~1-2 s to load but that is i think due to local hosting, but overall the issue is gone

@yashranaway

Copy link
Copy Markdown
Contributor Author

also some components have rendering issues (not related to this PR), but i'll make a pr to fix that as well

image

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/playground/auto-discovery.ts (1)

94-126: Consider reducing console verbosity in production.

The multi-path import strategy is excellent and the logging helps debugging. However, the extensive console.log statements (lines 97, 102, 111, 116) may clutter production consoles.

Consider wrapping debug logs in a condition:

const DEBUG = process.env.NODE_ENV === 'development';

if (DEBUG) console.log(`[Import] Attempting: @/components/billingsdk/${componentName}`);

Or use a logging library with configurable levels.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18ef7d3 and 11a3d1b.

📒 Files selected for processing (2)
  • src/components/playground/auto-discovery.ts (1 hunks)
  • src/components/playground/playground-header.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/playground/playground-header.tsx
🧰 Additional context used
🪛 ast-grep (0.39.6)
src/components/playground/auto-discovery.ts

[warning] 70-73: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
<${escapedName}[^>]{0,500}(?:>|\\/>)(?:[^<]{0,2000}|(?:<(?!\\/${escapedName}>)[^>]{0,100}>){0,50})*?(?:<\\/${escapedName}>|(?=\\n\\s*\\))),
'i'
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Typecheck and Build
🔇 Additional comments (6)
src/components/playground/auto-discovery.ts (6)

1-28: LGTM! Clean type definitions and fetch logic.

The registry interfaces are well-structured, and fetchRegistryManifest has appropriate error handling.


41-58: LGTM! Utility functions are well-implemented.

The PascalCase conversion, categorization logic, and regex escaping are all correct.


60-92: Regex patterns have good ReDoS protections, but remain complex.

The mitigations (50KB input cap, escaped variables, bounded quantifiers) significantly reduce ReDoS risk. However, the nested quantifiers with negative lookahead (line 72) can still cause backtracking on pathological inputs. The current safeguards are acceptable for production use.

Consider testing the regex with edge cases such as deeply nested component trees or malformed JSX to confirm performance remains acceptable. Based on static analysis hints


128-182: LGTM! Robust parallel discovery with proper error handling.

The use of Promise.allSettled ensures all components are processed even if some fail, and per-item error handling is comprehensive.


184-193: Excellent normalization logic that handles all edge cases.

The multi-step normalization correctly converts PascalCase, handles numbers, and cleans up edge cases. This fully addresses the previous review feedback.


195-224: LGTM! Robust fallback strategy for missing metadata.

The function correctly proceeds with sensible defaults when metadata is unavailable, only returning null if the component import itself fails. This addresses previous review feedback.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/components/playground/auto-discovery.ts (2)

101-133: Consider adding a debug flag or using a logging library.

The function contains extensive console logging (lines 104, 109, 113, 115, 118, 123, 127, 129, 131) that may be verbose in production. Consider wrapping these in a debug flag or using a proper logging library with configurable levels.


55-61: Categorization logic could be more maintainable.

The cascading if statements with keyword checks work but could be refactored into a configuration map for easier maintenance and extension.

Example structure:

const categoryPatterns = [
  { patterns: ['pricing-table'], category: 'pricing' },
  { patterns: ['subscription', 'plan', 'cancel', 'update'], category: 'subscription' },
  { patterns: ['payment', 'invoice', 'billing'], category: 'payment' },
  { patterns: ['usage', 'meter', 'proration', 'upcoming-charges', 'detailed-usage'], category: 'usage' },
];

function categorizeComponent(name: string): string {
  for (const { patterns, category } of categoryPatterns) {
    if (patterns.some(p => name.includes(p))) return category;
  }
  return 'ui';
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11a3d1b and 4ead29a.

📒 Files selected for processing (1)
  • src/components/playground/auto-discovery.ts (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.39.6)
src/components/playground/auto-discovery.ts

[warning] 77-80: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
<${escapedName}[^>]{0,500}(?:>|\\/>)(?:[^<]{0,2000}|(?:<(?!\\/${escapedName}>)[^>]{0,100}>){0,50})*?(?:<\\/${escapedName}>|(?=\\n\\s*\\))),
'i'
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Typecheck and Build
🔇 Additional comments (4)
src/components/playground/auto-discovery.ts (4)

19-24: Good refactor—DRY principle applied.

Extracting the filtering predicate into a reusable helper addresses the previous feedback about duplicate logic.


63-99: Regex safety mitigations in place; static analysis warning persists.

The implementation now includes proper escaping (line 77), content size limits (line 75), and bounded quantifiers (lines 78-80, 87) to mitigate ReDoS risks. The static analysis warning at lines 77-80 is expected when constructing regex from variables, but the mitigations reduce the actual risk.


186-195: Robust normalization—addresses previous feedback.

The implementation now properly handles PascalCase/camelCase → kebab-case conversion (line 188), letter-number transitions (line 189), and edge cases like multiple hyphens and trimming.


197-226: Graceful fallback for missing metadata—good improvement.

The function now proceeds with sensible defaults when metadata is missing (lines 202-204) and uses optional chaining (lines 212-213, 217) to safely access metadata properties. This ensures deep links work even when metadata fetch fails.

@yashranaway

Copy link
Copy Markdown
Contributor Author

@tsahil01 can you take a look

@yashranaway

Copy link
Copy Markdown
Contributor Author

@tsahil01 any update?

@thepushkaraj

Copy link
Copy Markdown
Contributor

lgtm @yashranaway!

There are some issues when loading certain components, though. It may not be because of this PR, but we will need to consider that as well.

image

Just cleanup the console logs and I think we should be good to go

@yashranaway

Copy link
Copy Markdown
Contributor Author

lgtm @yashranaway!

There are some issues when loading certain components, though. It may not be because of this PR, but we will need to consider that as well.

image

Just cleanup the console logs and I think we should be good to go

That is actually planned for a separate PR since not all components are playground compatible I'll be making a new PR fixing all of them I'll cover this in that.

@thepushkaraj thepushkaraj left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm! @yashranaway please cleanup those console.logs as well when switching a component

@yashranaway

Copy link
Copy Markdown
Contributor Author

lgtm! @yashranaway please cleanup those console.logs as well when switching a component

done

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/proration-preview-demo.tsx (1)

97-104: Add WebKit scrollbar hiding and consider accessibility.

The inline styles hide scrollbars for Firefox and IE/Edge but not for Chrome/Safari (WebKit browsers). Additionally, hiding scrollbars without a visual cue (fade effect, shadow, or indicator) may reduce discoverability for users who need to know additional tabs exist.

To hide scrollbars across all browsers, add a global CSS rule targeting this component:

.scrollable-tabs::-webkit-scrollbar {
  display: none;
}

Then update the JSX:

-        <TabsList className="flex w-full overflow-x-auto gap-1 p-1 rounded-lg" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
+        <TabsList className="scrollable-tabs flex w-full overflow-x-auto gap-1 p-1 rounded-lg" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>

Additionally, consider adding a subtle fade or shadow at the edges to indicate scrollability:

-      <div className="overflow-hidden">
+      <div className="relative overflow-hidden after:absolute after:right-0 after:top-0 after:h-full after:w-8 after:bg-gradient-to-l after:from-background after:to-transparent after:pointer-events-none">
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ead29a and 3fbd312.

📒 Files selected for processing (2)
  • src/components/proration-preview-demo.tsx (7 hunks)
  • src/registry/billingsdk/demo/proration-preview-demo.tsx (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-03T13:12:09.970Z
Learnt from: rajdesai17
Repo: dodopayments/billingsdk PR: 278
File: src/registry/billingsdk/demo/limited-offer-dialog-demo.tsx:1-55
Timestamp: 2025-10-03T13:12:09.970Z
Learning: In the dodopayments/billingsdk repository, demo files should be duplicated in both `src/components/` and `src/registry/billingsdk/demo/` directories with identical implementations. Do not suggest re-exporting or consolidating these demo files.

Applied to files:

  • src/registry/billingsdk/demo/proration-preview-demo.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Typecheck and Build
🔇 Additional comments (1)
src/components/proration-preview-demo.tsx (1)

122-123: LGTM — No-op callbacks appropriate for demo.

Replacing console.log with empty callbacks removes unnecessary side effects in the demo and aligns with the auto-discovery system's cleaner demo behavior.

Also applies to: 153-154, 176-177, 197-198, 220-221, 241-242

@aagarwal1012
aagarwal1012 merged commit fbae90a into dodopayments:main Nov 7, 2025
1 of 2 checks passed
@yashranaway
yashranaway deleted the playground-auto-discovery branch November 7, 2025 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants