Skip to content

Commit e2b007f

Browse files
fix(wrapDefaultExport): handle complex multiline exports (#1624)
1 parent 7147ded commit e2b007f

File tree

3 files changed

+156
-23
lines changed

3 files changed

+156
-23
lines changed

.changeset/better-kings-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"flowbite-react": patch
3+
---
4+
5+
fix(wrapDefaultExport): handle complex multiline exports

packages/ui/src/cli/utils/wrap-default-export.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,90 @@ export const a = 1;
105105
module.exports = wrapper(myFunction);`;
106106
expect(wrapDefaultExport(input, "wrapper").trim()).toBe(expected.trim());
107107
});
108+
109+
it("handles complex multiline exports with nested function calls and objects", () => {
110+
const input = `import { withHOC } from "./utils";
111+
import type { Config } from "./types";
112+
113+
const config: Config = {
114+
/* config options here */
115+
};
116+
117+
export default withHOC(config, {
118+
feature1: true,
119+
feature2: "enabled",
120+
callbacks: {
121+
onInit: () => console.log("initialized"),
122+
onUpdate: (data) => console.log("updated:", data)
123+
},
124+
options: [
125+
{ id: 1, value: "first" },
126+
{ id: 2, value: "second" }
127+
]
128+
});`;
129+
130+
const expected = `import { withHOC } from "./utils";
131+
import type { Config } from "./types";
132+
133+
const config: Config = {
134+
/* config options here */
135+
};
136+
137+
export default wrapper(withHOC(config, {
138+
feature1: true,
139+
feature2: "enabled",
140+
callbacks: {
141+
onInit: () => console.log("initialized"),
142+
onUpdate: (data) => console.log("updated:", data)
143+
},
144+
options: [
145+
{ id: 1, value: "first" },
146+
{ id: 2, value: "second" }
147+
]
148+
}));`;
149+
150+
expect(wrapDefaultExport(input, "wrapper")).toBe(expected);
151+
});
152+
153+
it("handles complex multiline CJS exports with nested function calls and objects", () => {
154+
const input = `const { withHOC } = require("./utils");
155+
156+
const config = {
157+
/* config options here */
158+
};
159+
160+
module.exports = withHOC(config, {
161+
feature1: true,
162+
feature2: "enabled",
163+
callbacks: {
164+
onInit: () => console.log("initialized"),
165+
onUpdate: (data) => console.log("updated:", data)
166+
},
167+
options: [
168+
{ id: 1, value: "first" },
169+
{ id: 2, value: "second" }
170+
]
171+
});`;
172+
173+
const expected = `const { withHOC } = require("./utils");
174+
175+
const config = {
176+
/* config options here */
177+
};
178+
179+
module.exports = wrapper(withHOC(config, {
180+
feature1: true,
181+
feature2: "enabled",
182+
callbacks: {
183+
onInit: () => console.log("initialized"),
184+
onUpdate: (data) => console.log("updated:", data)
185+
},
186+
options: [
187+
{ id: 1, value: "first" },
188+
{ id: 2, value: "second" }
189+
]
190+
}));`;
191+
192+
expect(wrapDefaultExport(input, "wrapper")).toBe(expected);
193+
});
108194
});

packages/ui/src/cli/utils/wrap-default-export.ts

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,75 @@ export function wrapDefaultExport(content: string, withFunction: string): string
1616

1717
let wrappedContent = content;
1818

19-
// Handle ESM exports
20-
if (isESM) {
21-
const esmMatch = content.match(/export\s+default\s+(?:class|interface|abstract\s+class)\s+/);
22-
if (!esmMatch) {
23-
wrappedContent = wrappedContent.replace(
24-
/(export\s+default\s+)([^;\n]+(?:{[^}]*})?[^;\n]*)(;?\s*)$/gm,
25-
(_, prefix, exportValue, semicolon) => {
26-
const trimmedValue = exportValue.trim();
27-
return `${prefix}${withFunction}(${trimmedValue})${semicolon}`;
28-
},
29-
);
19+
const config = isCJS ? EXPORT_CONFIGS.cjs : EXPORT_CONFIGS.esm;
20+
21+
// Skip if it's a class/interface export
22+
if (!content.match(config.skipPattern)) {
23+
const lastExportMatch = wrappedContent.match(config.matchPattern);
24+
if (lastExportMatch) {
25+
const [fullMatch, prefix, rest] = lastExportMatch;
26+
const { exportValue, restContent } = extractExportValue(rest);
27+
const replacement = createWrappedReplacement(prefix, exportValue, withFunction, restContent);
28+
29+
// Replace only the last occurrence
30+
const index = wrappedContent.lastIndexOf(fullMatch);
31+
wrappedContent = wrappedContent.slice(0, index) + replacement + wrappedContent.slice(index + fullMatch.length);
3032
}
3133
}
3234

33-
// Handle CJS exports
34-
if (isCJS) {
35-
const cjsMatch = content.match(/module\.exports\s*=\s*(?:class|interface|abstract\s+class)\s+/);
36-
if (!cjsMatch) {
37-
wrappedContent = wrappedContent.replace(
38-
/(module\.exports\s*=\s*)([^;\n]+(?:{[^}]*})?[^;\n]*)(;?\s*)$/gm,
39-
(_, prefix, exportValue, semicolon) => {
40-
const trimmedValue = exportValue.trim();
41-
return `${prefix}${withFunction}(${trimmedValue})${semicolon}`;
42-
},
43-
);
35+
return wrappedContent;
36+
}
37+
38+
const EXPORT_CONFIGS = {
39+
esm: {
40+
skipPattern: /export\s+default\s+(?:class|interface|abstract\s+class)\s+/,
41+
matchPattern: /(export\s+default\s+)([\s\S]*$)/m,
42+
},
43+
cjs: {
44+
skipPattern: /module\.exports\s*=\s*(?:class|interface|abstract\s+class)\s+/,
45+
matchPattern: /(module\.exports\s*=\s*)([\s\S]*$)/m,
46+
},
47+
};
48+
49+
/**
50+
* Extracts the export value from a string, handling nested structures
51+
*/
52+
function extractExportValue(rest: string): { exportValue: string; restContent: string } {
53+
let depth = 0;
54+
let i = 0;
55+
56+
// Parse the export value handling nested parentheses and braces
57+
for (i = 0; i < rest.length; i++) {
58+
const char = rest[i];
59+
if (char === "(" || char === "{") depth++;
60+
if (char === ")" || char === "}") depth--;
61+
62+
// Break on semicolon or newline if we're not inside parentheses/braces
63+
if (depth === 0 && (char === ";" || char === "\n")) {
64+
return {
65+
exportValue: rest.slice(0, char === ";" ? i + 1 : i),
66+
restContent: rest.slice(char === ";" ? i + 1 : i),
67+
};
4468
}
4569
}
4670

47-
return wrappedContent;
71+
// If we didn't find a terminator, use the whole rest
72+
return {
73+
exportValue: rest,
74+
restContent: "",
75+
};
76+
}
77+
78+
/**
79+
* Creates a wrapped replacement for an export statement
80+
*/
81+
function createWrappedReplacement(
82+
prefix: string,
83+
exportValue: string,
84+
withFunction: string,
85+
restContent: string,
86+
): string {
87+
const trimmedValue = exportValue.trim();
88+
const hasTrailingSemi = trimmedValue.endsWith(";");
89+
return `${prefix}${withFunction}(${trimmedValue.replace(/;$/, "")})${hasTrailingSemi ? ";" : ""}${restContent}`;
4890
}

0 commit comments

Comments
 (0)