Skip to content

Commit 9017065

Browse files
fix(ui/cli/setup-init/compare-nodes): more styling format support (#1594)
* fix(compareNodes): support [different quote styles, with and without semicolons] - add tests * add changeset
1 parent 29496c4 commit 9017065

File tree

5 files changed

+240
-35
lines changed

5 files changed

+240
-35
lines changed

.changeset/funny-lights-march.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"flowbite-react": patch
3+
---
4+
5+
fix(compareNodes): support:
6+
7+
- different quote styles
8+
- with and without semicolons
9+
- trailing commas in objects and arrays
10+
11+
- add tests

bun.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
},
111111
"packages/ui": {
112112
"name": "flowbite-react",
113-
"version": "0.11.9",
113+
"version": "0.12.0",
114114
"bin": {
115115
"flowbite-react": "./dist/cli/bin.js",
116116
},

packages/ui/src/cli/commands/setup-init.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "fs/promises";
22
import { parse } from "recast";
33
import { initFilePath, initJsxFilePath } from "../consts";
4+
import { compareNodes } from "../utils/compare-nodes";
45
import type { Config } from "./setup-config";
56

67
/**
@@ -68,37 +69,3 @@ ThemeInit.displayName = "ThemeInit";
6869
console.error(`Failed to update ${targetPath}:`, error);
6970
}
7071
}
71-
72-
/**
73-
* Compare two AST nodes ignoring location info and comments
74-
*/
75-
function compareNodes(a: unknown, b: unknown): boolean {
76-
if (a === b) {
77-
return true;
78-
}
79-
if (!a || !b) {
80-
return false;
81-
}
82-
if (Array.isArray(a)) {
83-
if (!Array.isArray(b) || a.length !== b.length) {
84-
return false;
85-
}
86-
return a.every((item, i) => compareNodes(item, b[i]));
87-
}
88-
if (typeof a !== "object" || typeof b !== "object") {
89-
return a === b;
90-
}
91-
92-
// Skip location and comment-related properties
93-
const keysA = Object.keys(a).filter(
94-
(k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
95-
);
96-
const keysB = Object.keys(b).filter(
97-
(k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
98-
);
99-
100-
if (keysA.length !== keysB.length) {
101-
return false;
102-
}
103-
return keysA.every((key) => compareNodes(a[key as keyof typeof a], b[key as keyof typeof b]));
104-
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { parse } from "recast";
2+
import { describe, expect, it } from "vitest";
3+
import { compareNodes } from "./compare-nodes";
4+
5+
describe("compareNodes", () => {
6+
it("should handle basic equality", () => {
7+
expect(compareNodes(null, null)).toBe(true);
8+
expect(compareNodes(undefined, undefined)).toBe(true);
9+
expect(compareNodes(42, 42)).toBe(true);
10+
expect(compareNodes("hello", "hello")).toBe(true);
11+
expect(compareNodes(null, undefined)).toBe(false);
12+
expect(compareNodes(42, "42")).toBe(false);
13+
});
14+
15+
it("should compare arrays correctly", () => {
16+
expect(compareNodes([1, 2, 3], [1, 2, 3])).toBe(true);
17+
expect(compareNodes([1, 2, 3], [1, 2])).toBe(false);
18+
expect(compareNodes([1, 2, 3], [1, 2, 4])).toBe(false);
19+
expect(compareNodes([], [])).toBe(true);
20+
});
21+
22+
it("should handle string literals with different quote styles", () => {
23+
const singleQuotes = parse("const x = 'hello';").program.body[0];
24+
const doubleQuotes = parse('const x = "hello";').program.body[0];
25+
expect(compareNodes(singleQuotes, doubleQuotes)).toBe(true);
26+
});
27+
28+
it("should handle complex AST nodes with string literals", () => {
29+
const code1 = `
30+
const config = {
31+
dark: true,
32+
prefix: 'tw-',
33+
version: 1
34+
};
35+
`;
36+
37+
const code2 = `
38+
const config = {
39+
dark: true,
40+
prefix: "tw-",
41+
version: 1
42+
};
43+
`;
44+
45+
const ast1 = parse(code1).program;
46+
const ast2 = parse(code2).program;
47+
expect(compareNodes(ast1, ast2)).toBe(true);
48+
});
49+
50+
it("should handle different AST node types", () => {
51+
const ast1 = parse("const x = 'hello';").program;
52+
const ast2 = parse("const x = 42;").program;
53+
expect(compareNodes(ast1, ast2)).toBe(false);
54+
});
55+
56+
it("should ignore location and comment properties", () => {
57+
const code1 = `
58+
// This is a comment
59+
const x = 'hello';
60+
`;
61+
62+
const code2 = `
63+
/* Different comment */
64+
const x = "hello";
65+
`;
66+
67+
const ast1 = parse(code1).program;
68+
const ast2 = parse(code2).program;
69+
expect(compareNodes(ast1, ast2)).toBe(true);
70+
});
71+
72+
it("should handle template literals", () => {
73+
const code1 = "const x = `hello`;";
74+
const code2 = "const x = 'hello';";
75+
const ast1 = parse(code1).program;
76+
const ast2 = parse(code2).program;
77+
expect(compareNodes(ast1, ast2)).toBe(false); // Template literals should be treated differently
78+
});
79+
80+
it("should handle object properties with different quote styles", () => {
81+
const code1 = `
82+
const obj = {
83+
'key': 'value',
84+
"another-key": "value"
85+
};
86+
`;
87+
88+
const code2 = `
89+
const obj = {
90+
"key": "value",
91+
'another-key': 'value'
92+
};
93+
`;
94+
95+
const ast1 = parse(code1).program;
96+
const ast2 = parse(code2).program;
97+
expect(compareNodes(ast1, ast2)).toBe(true);
98+
});
99+
100+
it("should handle code with and without semicolons", () => {
101+
// Variable declarations
102+
const withSemi = parse("const x = 42;").program;
103+
const withoutSemi = parse("const x = 42").program;
104+
expect(compareNodes(withSemi, withoutSemi)).toBe(true);
105+
106+
// Multiple statements
107+
const multiWithSemi = parse("const x = 1; const y = 2;").program;
108+
const multiWithoutSemi = parse("const x = 1\nconst y = 2").program;
109+
expect(compareNodes(multiWithSemi, multiWithoutSemi)).toBe(true);
110+
111+
// Object declarations
112+
const objWithSemi = parse("const obj = { a: 1, b: 2 };").program;
113+
const objWithoutSemi = parse("const obj = { a: 1, b: 2 }").program;
114+
expect(compareNodes(objWithSemi, objWithoutSemi)).toBe(true);
115+
116+
// Function declarations
117+
const funcWithSemi = parse("function test() { return 42; }").program;
118+
const funcWithoutSemi = parse("function test() { return 42 }").program;
119+
expect(compareNodes(funcWithSemi, funcWithoutSemi)).toBe(true);
120+
});
121+
122+
it("should handle trailing commas in objects and arrays", () => {
123+
// Single-line objects
124+
const objNoComma = parse("const obj = { a: 1, b: 2 }").program;
125+
const objWithComma = parse("const obj = { a: 1, b: 2, }").program;
126+
expect(compareNodes(objNoComma, objWithComma)).toBe(true);
127+
128+
// Multi-line objects
129+
const multilineObjNoComma = parse(`
130+
const obj = {
131+
a: 1,
132+
b: 2
133+
}
134+
`).program;
135+
const multilineObjWithComma = parse(`
136+
const obj = {
137+
a: 1,
138+
b: 2,
139+
}
140+
`).program;
141+
expect(compareNodes(multilineObjNoComma, multilineObjWithComma)).toBe(true);
142+
143+
// Single-line arrays
144+
const arrayNoComma = parse("const arr = [1, 2, 3]").program;
145+
const arrayWithComma = parse("const arr = [1, 2, 3,]").program;
146+
expect(compareNodes(arrayNoComma, arrayWithComma)).toBe(true);
147+
148+
// Multi-line arrays
149+
const multilineArrayNoComma = parse(`
150+
const arr = [
151+
1,
152+
2,
153+
3
154+
]
155+
`).program;
156+
const multilineArrayWithComma = parse(`
157+
const arr = [
158+
1,
159+
2,
160+
3,
161+
]
162+
`).program;
163+
expect(compareNodes(multilineArrayNoComma, multilineArrayWithComma)).toBe(true);
164+
165+
// Nested structures
166+
const nestedNoComma = parse(`
167+
const nested = {
168+
obj: { a: 1, b: 2 },
169+
arr: [1, 2, 3]
170+
}
171+
`).program;
172+
const nestedWithComma = parse(`
173+
const nested = {
174+
obj: { a: 1, b: 2, },
175+
arr: [1, 2, 3,],
176+
}
177+
`).program;
178+
expect(compareNodes(nestedNoComma, nestedWithComma)).toBe(true);
179+
});
180+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Compare two AST nodes ignoring location info and comments
3+
*/
4+
export function compareNodes(a: unknown, b: unknown): boolean {
5+
if (a === b) {
6+
return true;
7+
}
8+
if (!a || !b) {
9+
return false;
10+
}
11+
if (Array.isArray(a)) {
12+
if (!Array.isArray(b) || a.length !== b.length) {
13+
return false;
14+
}
15+
return a.every((item, i) => compareNodes(item, b[i]));
16+
}
17+
if (typeof a !== "object" || typeof b !== "object") {
18+
return a === b;
19+
}
20+
21+
// Handle string literals specially - normalize quotes
22+
if (
23+
"type" in a &&
24+
"type" in b &&
25+
(a.type === "StringLiteral" || a.type === "Literal") &&
26+
(b.type === "StringLiteral" || b.type === "Literal") &&
27+
"value" in a &&
28+
"value" in b &&
29+
typeof a.value === "string" &&
30+
typeof b.value === "string"
31+
) {
32+
return a.value === b.value;
33+
}
34+
35+
// Skip location and comment-related properties
36+
const keysA = Object.keys(a).filter(
37+
(k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
38+
);
39+
const keysB = Object.keys(b).filter(
40+
(k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
41+
);
42+
43+
if (keysA.length !== keysB.length) {
44+
return false;
45+
}
46+
return keysA.every((key) => compareNodes(a[key as keyof typeof a], b[key as keyof typeof b]));
47+
}

0 commit comments

Comments
 (0)