Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/funny-lights-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"flowbite-react": patch
---

fix(compareNodes): support:

- different quote styles
- with and without semicolons
- trailing commas in objects and arrays

- add tests
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 1 addition & 34 deletions packages/ui/src/cli/commands/setup-init.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "fs/promises";
import { parse } from "recast";
import { initFilePath, initJsxFilePath } from "../consts";
import { compareNodes } from "../utils/compare-nodes";
import type { Config } from "./setup-config";

/**
Expand Down Expand Up @@ -68,37 +69,3 @@ ThemeInit.displayName = "ThemeInit";
console.error(`Failed to update ${targetPath}:`, error);
}
}

/**
* Compare two AST nodes ignoring location info and comments
*/
function compareNodes(a: unknown, b: unknown): boolean {
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) {
return false;
}
return a.every((item, i) => compareNodes(item, b[i]));
}
if (typeof a !== "object" || typeof b !== "object") {
return a === b;
}

// Skip location and comment-related properties
const keysA = Object.keys(a).filter(
(k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
);
const keysB = Object.keys(b).filter(
(k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
);

if (keysA.length !== keysB.length) {
return false;
}
return keysA.every((key) => compareNodes(a[key as keyof typeof a], b[key as keyof typeof b]));
}
180 changes: 180 additions & 0 deletions packages/ui/src/cli/utils/compare-nodes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { parse } from "recast";
import { describe, expect, it } from "vitest";
import { compareNodes } from "./compare-nodes";

describe("compareNodes", () => {
it("should handle basic equality", () => {
expect(compareNodes(null, null)).toBe(true);
expect(compareNodes(undefined, undefined)).toBe(true);
expect(compareNodes(42, 42)).toBe(true);
expect(compareNodes("hello", "hello")).toBe(true);
expect(compareNodes(null, undefined)).toBe(false);
expect(compareNodes(42, "42")).toBe(false);
});

it("should compare arrays correctly", () => {
expect(compareNodes([1, 2, 3], [1, 2, 3])).toBe(true);
expect(compareNodes([1, 2, 3], [1, 2])).toBe(false);
expect(compareNodes([1, 2, 3], [1, 2, 4])).toBe(false);
expect(compareNodes([], [])).toBe(true);
});

it("should handle string literals with different quote styles", () => {
const singleQuotes = parse("const x = 'hello';").program.body[0];
const doubleQuotes = parse('const x = "hello";').program.body[0];
expect(compareNodes(singleQuotes, doubleQuotes)).toBe(true);
});

it("should handle complex AST nodes with string literals", () => {
const code1 = `
const config = {
dark: true,
prefix: 'tw-',
version: 1
};
`;

const code2 = `
const config = {
dark: true,
prefix: "tw-",
version: 1
};
`;

const ast1 = parse(code1).program;
const ast2 = parse(code2).program;
expect(compareNodes(ast1, ast2)).toBe(true);
});

it("should handle different AST node types", () => {
const ast1 = parse("const x = 'hello';").program;
const ast2 = parse("const x = 42;").program;
expect(compareNodes(ast1, ast2)).toBe(false);
});

it("should ignore location and comment properties", () => {
const code1 = `
// This is a comment
const x = 'hello';
`;

const code2 = `
/* Different comment */
const x = "hello";
`;

const ast1 = parse(code1).program;
const ast2 = parse(code2).program;
expect(compareNodes(ast1, ast2)).toBe(true);
});

it("should handle template literals", () => {
const code1 = "const x = `hello`;";
const code2 = "const x = 'hello';";
const ast1 = parse(code1).program;
const ast2 = parse(code2).program;
expect(compareNodes(ast1, ast2)).toBe(false); // Template literals should be treated differently
});

it("should handle object properties with different quote styles", () => {
const code1 = `
const obj = {
'key': 'value',
"another-key": "value"
};
`;

const code2 = `
const obj = {
"key": "value",
'another-key': 'value'
};
`;

const ast1 = parse(code1).program;
const ast2 = parse(code2).program;
expect(compareNodes(ast1, ast2)).toBe(true);
});

it("should handle code with and without semicolons", () => {
// Variable declarations
const withSemi = parse("const x = 42;").program;
const withoutSemi = parse("const x = 42").program;
expect(compareNodes(withSemi, withoutSemi)).toBe(true);

// Multiple statements
const multiWithSemi = parse("const x = 1; const y = 2;").program;
const multiWithoutSemi = parse("const x = 1\nconst y = 2").program;
expect(compareNodes(multiWithSemi, multiWithoutSemi)).toBe(true);

// Object declarations
const objWithSemi = parse("const obj = { a: 1, b: 2 };").program;
const objWithoutSemi = parse("const obj = { a: 1, b: 2 }").program;
expect(compareNodes(objWithSemi, objWithoutSemi)).toBe(true);

// Function declarations
const funcWithSemi = parse("function test() { return 42; }").program;
const funcWithoutSemi = parse("function test() { return 42 }").program;
expect(compareNodes(funcWithSemi, funcWithoutSemi)).toBe(true);
});

it("should handle trailing commas in objects and arrays", () => {
// Single-line objects
const objNoComma = parse("const obj = { a: 1, b: 2 }").program;
const objWithComma = parse("const obj = { a: 1, b: 2, }").program;
expect(compareNodes(objNoComma, objWithComma)).toBe(true);

// Multi-line objects
const multilineObjNoComma = parse(`
const obj = {
a: 1,
b: 2
}
`).program;
const multilineObjWithComma = parse(`
const obj = {
a: 1,
b: 2,
}
`).program;
expect(compareNodes(multilineObjNoComma, multilineObjWithComma)).toBe(true);

// Single-line arrays
const arrayNoComma = parse("const arr = [1, 2, 3]").program;
const arrayWithComma = parse("const arr = [1, 2, 3,]").program;
expect(compareNodes(arrayNoComma, arrayWithComma)).toBe(true);

// Multi-line arrays
const multilineArrayNoComma = parse(`
const arr = [
1,
2,
3
]
`).program;
const multilineArrayWithComma = parse(`
const arr = [
1,
2,
3,
]
`).program;
expect(compareNodes(multilineArrayNoComma, multilineArrayWithComma)).toBe(true);

// Nested structures
const nestedNoComma = parse(`
const nested = {
obj: { a: 1, b: 2 },
arr: [1, 2, 3]
}
`).program;
const nestedWithComma = parse(`
const nested = {
obj: { a: 1, b: 2, },
arr: [1, 2, 3,],
}
`).program;
expect(compareNodes(nestedNoComma, nestedWithComma)).toBe(true);
});
});
47 changes: 47 additions & 0 deletions packages/ui/src/cli/utils/compare-nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Compare two AST nodes ignoring location info and comments
*/
export function compareNodes(a: unknown, b: unknown): boolean {
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) {
return false;
}
return a.every((item, i) => compareNodes(item, b[i]));
}
if (typeof a !== "object" || typeof b !== "object") {
return a === b;
}

// Handle string literals specially - normalize quotes
if (
"type" in a &&
"type" in b &&
(a.type === "StringLiteral" || a.type === "Literal") &&
(b.type === "StringLiteral" || b.type === "Literal") &&
"value" in a &&
"value" in b &&
typeof a.value === "string" &&
typeof b.value === "string"
) {
return a.value === b.value;
}

// Skip location and comment-related properties
const keysA = Object.keys(a).filter(
(k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
);
const keysB = Object.keys(b).filter(
(k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
);

if (keysA.length !== keysB.length) {
return false;
}
return keysA.every((key) => compareNodes(a[key as keyof typeof a], b[key as keyof typeof b]));
}
Loading