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
@changeset-bot
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

@coderabbitai
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.

@vercel
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