Skip to content

Conversation

SutuSebastian
Copy link
Collaborator

@SutuSebastian SutuSebastian commented Sep 8, 2025

Summary by CodeRabbit

  • Bug Fixes
    • Correct handling of complex multiline default exports across ESM and CommonJS, ensuring proper wrapping without affecting classes or interfaces and preserving surrounding formatting.
  • Tests
    • Added test coverage for multiline export scenarios in both ESM and CommonJS to prevent regressions.
  • Chores
    • Added a patch release entry for flowbite-react.

@SutuSebastian SutuSebastian self-assigned this Sep 8, 2025
@SutuSebastian SutuSebastian added 🐛 bug Something isn't working confirmed This bug was confirmed labels Sep 8, 2025
@SutuSebastian SutuSebastian linked an issue Sep 8, 2025 that may be closed by this pull request
2 tasks
Copy link

changeset-bot bot commented Sep 8, 2025

🦋 Changeset detected

Latest commit: 74f92b1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
flowbite-react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Walkthrough

Refactors wrapDefaultExport to a unified, config-driven flow handling ESM and CJS, adding robust export-value extraction, last-occurrence targeting, and class/interface skip logic. Adds tests covering complex multiline exports for both module systems. Introduces a changeset entry for a patch release noting the fix.

Changes

Cohort / File(s) Summary
Release notes
\.changeset/better-kings-fail.md
Adds a patch changeset for flowbite-react mentioning fix to wrapDefaultExport for complex multiline exports.
CLI utils tests
packages/ui/src/cli/utils/wrap-default-export.test.ts
Adds two tests validating wrapping of complex multiline default exports in ESM and CJS forms.
CLI utils implementation
packages/ui/src/cli/utils/wrap-default-export.ts
Replaces dual regex paths with a unified config-driven approach; adds EXPORT_CONFIGS, extractExportValue, createWrappedReplacement; targets last export occurrence; preserves trailing content; skips class/interface/abstract default exports.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Dev as Developer
    participant WDE as wrapDefaultExport(content, withFunction)
    participant CFG as EXPORT_CONFIGS
    participant EX as extractExportValue()
    participant CR as createWrappedReplacement()

    Dev->>WDE: Provide file content + withFunction
    WDE->>CFG: Determine module type (esm/cjs) patterns
    WDE->>WDE: Find last matching default export
    alt No match or default export is class/interface/abstract
        WDE-->>Dev: Return original content
    else Matched export value
        WDE->>EX: Parse export value (handle nested (), {})
        EX-->>WDE: exportValue + restContent
        WDE->>CR: Build wrapped replacement
        CR-->>WDE: prefix + withFunction(exportValue) + restContent
        WDE-->>Dev: Return modified content
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • rluders

Poem

I hop through code with whiskers bright,
Wrapping exports late at night.
ESM, CJS—no fright, no fuss,
I parse the braces on a bus.
With gentle paws, I fix the flow—
One last export, and off I go! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 1623-npx-flowbite-reactlatest-init-generates-a-syntax-error-sentry-with-nextjs

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.

Copy link

vercel bot commented Sep 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
flowbite-react Ready Ready Preview Comment Sep 8, 2025 0:58am
flowbite-react-storybook Ready Ready Preview Comment Sep 8, 2025 0:58am

@SutuSebastian SutuSebastian merged commit e2b007f into main Sep 8, 2025
8 checks passed
@SutuSebastian SutuSebastian deleted the 1623-npx-flowbite-reactlatest-init-generates-a-syntax-error-sentry-with-nextjs branch September 8, 2025 13:00
@github-actions github-actions bot mentioned this pull request Sep 8, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
.changeset/better-kings-fail.md (1)

5-5: Changeset looks good

Patch note is clear and scoped. Consider backticks around the function name for consistency: wrapDefaultExport → wrapDefaultExport.

-fix(wrapDefaultExport): handle complex multiline exports
+fix(`wrapDefaultExport`): handle complex multiline exports
packages/ui/src/cli/utils/wrap-default-export.ts (1)

38-47: Optional: make “last occurrence” explicit and faster

Define a lightweight prefixRegex (/export\s+default\s+/g or /module\.exports\s*=\s*/g) to avoid the initial greedy (.*$) match. Then iterate matchAll and pick the last index. This reduces work on large files.

Example config tweak (adjust main logic accordingly):

 const EXPORT_CONFIGS = {
   esm: {
     skipPattern: /export\s+default\s+(?:class|interface|abstract\s+class)\s+/,
-    matchPattern: /(export\s+default\s+)([\s\S]*$)/m,
+    matchPattern: /(export\s+default\s+)([\s\S]*$)/m,
+    prefixRegex: /export\s+default\s+/g,
   },
   cjs: {
     skipPattern: /module\.exports\s*=\s*(?:class|interface|abstract\s+class)\s+/,
-    matchPattern: /(module\.exports\s*=\s*)([\s\S]*$)/m,
+    matchPattern: /(module\.exports\s*=\s*)([\s\S]*$)/m,
+    prefixRegex: /module\.exports\s*=\s*/g,
   },
 };
packages/ui/src/cli/utils/wrap-default-export.test.ts (2)

109-151: Great coverage for complex multiline ESM exports; add a newline-after-default case

Current parser fails when a newline follows export default. Add a test to lock this in after fixing the parser.

@@
   it("handles complex multiline exports with nested function calls and objects", () => {
@@
   });
+
+  it("wraps ESM default export when a newline follows `export default`", () => {
+    const input = `
+export default
+  withHOC(config, {
+    enabled: true
+  });
+`;
+    const expected = `export default wrapper(withHOC(config, {
+    enabled: true
+  }));`;
+    expect(wrapDefaultExport(input, "wrapper").trim()).toBe(expected.trim());
+  });

153-193: CJS multiline test is good; add array and “last assignment wins” cases

  • Add an array default export to ensure [] depth is handled.
  • Add a case with multiple module.exports = assignments to verify we wrap the last one.
@@
   it("handles complex multiline CJS exports with nested function calls and objects", () => {
@@
   });
+
+  it("wraps multiline array ESM default export", () => {
+    const input = `export default [
+  1,
+  2
+];`;
+    const expected = `export default wrapper([
+  1,
+  2
+]);`;
+    expect(wrapDefaultExport(input, "wrapper")).toBe(expected);
+  });
+
+  it("wraps only the last CJS assignment", () => {
+    const input = `
+module.exports = first;
+module.exports = second;
+`;
+    const expected = `
+module.exports = wrapper(second);
+`;
+    expect(wrapDefaultExport(input, "wrapper").trim()).toBe(expected.trim());
+  });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7147ded and 74f92b1.

📒 Files selected for processing (3)
  • .changeset/better-kings-fail.md (1 hunks)
  • packages/ui/src/cli/utils/wrap-default-export.test.ts (1 hunks)
  • packages/ui/src/cli/utils/wrap-default-export.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/ui/src/cli/utils/wrap-default-export.test.ts (1)
packages/ui/src/cli/utils/wrap-default-export.ts (1)
  • wrapDefaultExport (10-36)

Comment on lines +21 to 33
// Skip if it's a class/interface export
if (!content.match(config.skipPattern)) {
const lastExportMatch = wrappedContent.match(config.matchPattern);
if (lastExportMatch) {
const [fullMatch, prefix, rest] = lastExportMatch;
const { exportValue, restContent } = extractExportValue(rest);
const replacement = createWrappedReplacement(prefix, exportValue, withFunction, restContent);

// Replace only the last occurrence
const index = wrappedContent.lastIndexOf(fullMatch);
wrappedContent = wrappedContent.slice(0, index) + replacement + wrappedContent.slice(index + fullMatch.length);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

"Last occurrence" logic doesn’t actually target the last export

match(config.matchPattern) captures from the first occurrence to EOF; replacing the “last occurrence” of fullMatch still uses the first prefix. This fails when multiple module.exports = (or rare multiple ESM defaults in generated code) appear; it won’t wrap the last one.

Apply this diff to locate the last prefix and compute rest from that position:

-  // Skip if it's a class/interface export
-  if (!content.match(config.skipPattern)) {
-    const lastExportMatch = wrappedContent.match(config.matchPattern);
-    if (lastExportMatch) {
-      const [fullMatch, prefix, rest] = lastExportMatch;
-      const { exportValue, restContent } = extractExportValue(rest);
-      const replacement = createWrappedReplacement(prefix, exportValue, withFunction, restContent);
-
-      // Replace only the last occurrence
-      const index = wrappedContent.lastIndexOf(fullMatch);
-      wrappedContent = wrappedContent.slice(0, index) + replacement + wrappedContent.slice(index + fullMatch.length);
-    }
-  }
+  // Skip if it's a class/interface export
+  if (!content.match(config.skipPattern)) {
+    const match = wrappedContent.match(config.matchPattern);
+    if (match) {
+      const [, prefix] = match;
+      const index = wrappedContent.lastIndexOf(prefix);
+      if (index !== -1) {
+        const restFromLast = wrappedContent.slice(index + prefix.length);
+        const { exportValue, restContent } = extractExportValue(restFromLast);
+        const replacement = createWrappedReplacement(prefix, exportValue, withFunction, restContent);
+        wrappedContent = wrappedContent.slice(0, index) + replacement;
+      }
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Skip if it's a class/interface export
if (!content.match(config.skipPattern)) {
const lastExportMatch = wrappedContent.match(config.matchPattern);
if (lastExportMatch) {
const [fullMatch, prefix, rest] = lastExportMatch;
const { exportValue, restContent } = extractExportValue(rest);
const replacement = createWrappedReplacement(prefix, exportValue, withFunction, restContent);
// Replace only the last occurrence
const index = wrappedContent.lastIndexOf(fullMatch);
wrappedContent = wrappedContent.slice(0, index) + replacement + wrappedContent.slice(index + fullMatch.length);
}
}
// Skip if it's a class/interface export
if (!content.match(config.skipPattern)) {
const match = wrappedContent.match(config.matchPattern);
if (match) {
const [, prefix] = match;
const index = wrappedContent.lastIndexOf(prefix);
if (index !== -1) {
const restFromLast = wrappedContent.slice(index + prefix.length);
const { exportValue, restContent } = extractExportValue(restFromLast);
const replacement = createWrappedReplacement(prefix, exportValue, withFunction, restContent);
wrappedContent = wrappedContent.slice(0, index) + replacement;
}
}
}

Comment on lines +52 to +76
function extractExportValue(rest: string): { exportValue: string; restContent: string } {
let depth = 0;
let i = 0;

// Parse the export value handling nested parentheses and braces
for (i = 0; i < rest.length; i++) {
const char = rest[i];
if (char === "(" || char === "{") depth++;
if (char === ")" || char === "}") depth--;

// Break on semicolon or newline if we're not inside parentheses/braces
if (depth === 0 && (char === ";" || char === "\n")) {
return {
exportValue: rest.slice(0, char === ";" ? i + 1 : i),
restContent: rest.slice(char === ";" ? i + 1 : i),
};
}
}

return wrappedContent;
// If we didn't find a terminator, use the whole rest
return {
exportValue: rest,
restContent: "",
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Parser misses arrays and breaks when a newline follows export default

  • Arrays ([]) aren’t tracked in depth, so multiline array exports split early on the first newline.
  • If there’s a newline immediately after export default (common in formatted code), exportValue becomes empty.

Apply this diff to track brackets and skip leading whitespace/newlines:

-function extractExportValue(rest: string): { exportValue: string; restContent: string } {
-  let depth = 0;
-  let i = 0;
-
-  // Parse the export value handling nested parentheses and braces
-  for (i = 0; i < rest.length; i++) {
-    const char = rest[i];
-    if (char === "(" || char === "{") depth++;
-    if (char === ")" || char === "}") depth--;
-
-    // Break on semicolon or newline if we're not inside parentheses/braces
-    if (depth === 0 && (char === ";" || char === "\n")) {
-      return {
-        exportValue: rest.slice(0, char === ";" ? i + 1 : i),
-        restContent: rest.slice(char === ";" ? i + 1 : i),
-      };
-    }
-  }
-
-  // If we didn't find a terminator, use the whole rest
-  return {
-    exportValue: rest,
-    restContent: "",
-  };
-}
+function extractExportValue(rest: string): { exportValue: string; restContent: string } {
+  let depth = 0;
+  let i = 0;
+
+  // Skip leading whitespace/newlines between the prefix and the expression
+  while (i < rest.length && /\s/.test(rest[i])) i++;
+  const start = i;
+
+  // Parse the export value handling nested (), {}, []
+  for (; i < rest.length; i++) {
+    const char = rest[i];
+    if (char === "(" || char === "{" || char === "[") depth++;
+    else if (char === ")" || char === "}" || char === "]") depth = Math.max(0, depth - 1);
+
+    // Break on semicolon or newline if we're not inside nested structures
+    if (depth === 0 && (char === ";" || char === "\n")) {
+      return {
+        exportValue: rest.slice(start, char === ";" ? i + 1 : i),
+        restContent: rest.slice(char === ";" ? i + 1 : i),
+      };
+    }
+  }
+
+  // If we didn't find a terminator, use the remaining substring
+  return {
+    exportValue: rest.slice(start),
+    restContent: "",
+  };
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function extractExportValue(rest: string): { exportValue: string; restContent: string } {
let depth = 0;
let i = 0;
// Parse the export value handling nested parentheses and braces
for (i = 0; i < rest.length; i++) {
const char = rest[i];
if (char === "(" || char === "{") depth++;
if (char === ")" || char === "}") depth--;
// Break on semicolon or newline if we're not inside parentheses/braces
if (depth === 0 && (char === ";" || char === "\n")) {
return {
exportValue: rest.slice(0, char === ";" ? i + 1 : i),
restContent: rest.slice(char === ";" ? i + 1 : i),
};
}
}
return wrappedContent;
// If we didn't find a terminator, use the whole rest
return {
exportValue: rest,
restContent: "",
};
}
function extractExportValue(rest: string): { exportValue: string; restContent: string } {
let depth = 0;
let i = 0;
// Skip leading whitespace/newlines between the prefix and the expression
while (i < rest.length && /\s/.test(rest[i])) i++;
const start = i;
// Parse the export value handling nested (), {}, []
for (; i < rest.length; i++) {
const char = rest[i];
if (char === "(" || char === "{" || char === "[") depth++;
else if (char === ")" || char === "}" || char === "]") depth = Math.max(0, depth - 1);
// Break on semicolon or newline if we're not inside nested structures
if (depth === 0 && (char === ";" || char === "\n")) {
return {
exportValue: rest.slice(start, char === ";" ? i + 1 : i),
restContent: rest.slice(char === ";" ? i + 1 : i),
};
}
}
// If we didn't find a terminator, use the remaining substring
return {
exportValue: rest.slice(start),
restContent: "",
};
}
🤖 Prompt for AI Agents
In packages/ui/src/cli/utils/wrap-default-export.ts around lines 52 to 76, the
parser doesn't track square brackets and treats an immediate newline after
"export default" as a terminator, producing an empty exportValue; update the
function to skip leading whitespace/newlines before parsing and include "[" and
"]" in the depth tracking logic (increment depth for "(", "{", "[" and decrement
for ")", "}", "]") so multiline arrays and formatted exports are handled
correctly, and only treat semicolon/newline as terminators when depth is zero
after skipping leading whitespace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐛 bug Something isn't working confirmed This bug was confirmed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

"npx flowbite-react@latest init" generates a syntax error Sentry with Next.js
1 participant