diff --git a/.changeset/dull-ducks-give.md b/.changeset/dull-ducks-give.md
new file mode 100644
index 000000000..59033c6d4
--- /dev/null
+++ b/.changeset/dull-ducks-give.md
@@ -0,0 +1,10 @@
+---
+"flowbite-react": patch
+---
+
+Search for `` in the project and warn if it's not found instead of warning all the time
+
+### Changes
+
+- during commands `build` and `dev` check files content for custom configuration and display a warning if `` is not found
+- switch tests in `src/cli` and `src/helpers` from `vitest` -> `bun:test`
diff --git a/packages/ui/package.json b/packages/ui/package.json
index e6e6db286..b44ba2830 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -257,7 +257,7 @@
"prepack": "clean-package",
"prepare": "bun run generate-metadata",
"prepublishOnly": "bun run build",
- "test": "bun test scripts && vitest",
+ "test": "bun test scripts src/cli src/helpers && vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
diff --git a/packages/ui/scripts/generate-metadata.test.ts b/packages/ui/scripts/generate-metadata.test.ts
index 0f46762db..3fe89ffc1 100644
--- a/packages/ui/scripts/generate-metadata.test.ts
+++ b/packages/ui/scripts/generate-metadata.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it } from "vitest";
+import { describe, expect, it } from "bun:test";
import { extractClassList, extractDependencyList } from "./generate-metadata";
describe("extractClassList", () => {
diff --git a/packages/ui/src/cli/commands/build.ts b/packages/ui/src/cli/commands/build.ts
index e15a88be5..d29a8ceb2 100644
--- a/packages/ui/src/cli/commands/build.ts
+++ b/packages/ui/src/cli/commands/build.ts
@@ -1,6 +1,7 @@
import fs from "fs/promises";
import { allowedExtensions, automaticClassGenerationMessage, classListFilePath, excludeDirs } from "../consts";
import { buildClassList } from "../utils/build-class-list";
+import { createInitLogger } from "../utils/create-init-logger";
import { extractComponentImports } from "../utils/extract-component-imports";
import { findFiles } from "../utils/find-files";
import { getConfig } from "../utils/get-config";
@@ -13,11 +14,24 @@ export async function build() {
try {
const config = await getConfig();
await setupInit(config);
+ const initLogger = createInitLogger(config);
const importedComponents: string[] = [];
if (config.components.length) {
console.warn(automaticClassGenerationMessage);
+
+ if (initLogger.isCustomConfig) {
+ const files = await findFiles({
+ patterns: allowedExtensions.map((ext) => `**/*${ext}`),
+ excludeDirs,
+ });
+
+ for (const file of files) {
+ const content = await fs.readFile(file, "utf-8");
+ initLogger.check(file, content);
+ }
+ }
} else {
const files = await findFiles({
patterns: allowedExtensions.map((ext) => `**/*${ext}`),
@@ -27,6 +41,7 @@ export async function build() {
for (const file of files) {
const content = await fs.readFile(file, "utf-8");
const components = extractComponentImports(content);
+ initLogger.check(file, content);
if (components.length) {
importedComponents.push(...components);
@@ -34,6 +49,8 @@ export async function build() {
}
}
+ initLogger.log();
+
const classList = buildClassList({
components: config.components.length ? config.components : [...new Set(importedComponents)],
dark: config.dark,
diff --git a/packages/ui/src/cli/commands/dev.ts b/packages/ui/src/cli/commands/dev.ts
index d93763ad8..7a9087465 100644
--- a/packages/ui/src/cli/commands/dev.ts
+++ b/packages/ui/src/cli/commands/dev.ts
@@ -13,6 +13,7 @@ import {
initJsxFilePath,
} from "../consts";
import { buildClassList } from "../utils/build-class-list";
+import { createInitLogger } from "../utils/create-init-logger";
import { extractComponentImports } from "../utils/extract-component-imports";
import { findFiles } from "../utils/find-files";
import { getClassList } from "../utils/get-class-list";
@@ -25,6 +26,7 @@ export async function dev() {
await setupOutputDirectory();
let config = await getConfig();
await setupInit(config);
+ const initLogger = createInitLogger(config);
if (config.components.length) {
console.warn(automaticClassGenerationMessage);
@@ -42,12 +44,15 @@ export async function dev() {
for (const file of files) {
const content = await fs.readFile(file, "utf-8");
const componentImports = extractComponentImports(content);
+ initLogger.check(file, content);
if (componentImports.length) {
importedComponentsMap[file] = componentImports;
}
}
+ initLogger.log();
+
const newImportedComponents = [...new Set(Object.values(importedComponentsMap).flat())];
const newClassList = buildClassList({
components: config.components.length ? config.components : newImportedComponents,
@@ -63,9 +68,19 @@ export async function dev() {
// watch for changes
async function handleChange(path: string, eventName: "change" | "unlink") {
+ if ([configFilePath, initFilePath, initJsxFilePath].includes(path)) {
+ config = await getConfig();
+ await setupInit(config);
+ initLogger.config = config;
+ }
+ if (path === gitIgnoreFilePath) {
+ await setupGitIgnore();
+ }
+
if (eventName === "change") {
const content = await fs.readFile(path, "utf-8");
const componentImports = extractComponentImports(content);
+ initLogger.check(path, content);
if (componentImports.length) {
importedComponentsMap[path] = componentImports;
@@ -75,17 +90,12 @@ export async function dev() {
}
if (eventName === "unlink") {
delete importedComponentsMap[path];
+ initLogger.checkedMap.delete(path);
}
- const newImportedComponents = [...new Set(Object.values(importedComponentsMap).flat())];
+ initLogger.log();
- if ([configFilePath, initFilePath, initJsxFilePath].includes(path)) {
- config = await getConfig();
- await setupInit(config);
- }
- if (path === gitIgnoreFilePath) {
- await setupGitIgnore();
- }
+ const newImportedComponents = [...new Set(Object.values(importedComponentsMap).flat())];
const newClassList = buildClassList({
components: config.components.length ? config.components : newImportedComponents,
diff --git a/packages/ui/src/cli/commands/setup-config.ts b/packages/ui/src/cli/commands/setup-config.ts
index b4572e89d..08db40630 100644
--- a/packages/ui/src/cli/commands/setup-config.ts
+++ b/packages/ui/src/cli/commands/setup-config.ts
@@ -3,36 +3,18 @@ import { klona } from "klona/json";
import { isEqual } from "../../helpers/is-equal";
import { COMPONENT_TO_CLASS_LIST_MAP } from "../../metadata/class-list";
import { configFilePath } from "../consts";
+import { createConfig, type Config } from "../utils/create-config";
import { getTailwindVersion } from "../utils/get-tailwind-version";
-export interface Config {
- $schema: string;
- components: string[];
- dark: boolean;
- path: string;
- prefix: string;
- rsc: boolean;
- tsx: boolean;
- version: 3 | 4;
-}
-
/**
* Sets up the `.flowbite-react/config.json` file in the project.
*
* This function creates or updates the configuration file with default values and validates existing configurations.
*/
export async function setupConfig(): Promise {
- const defaultConfig: Config = {
- $schema: "https://unpkg.com/flowbite-react/schema.json",
- components: [],
- dark: true,
- path: "src/components",
- // TODO: infer from project
- prefix: "",
- rsc: true,
- tsx: true,
+ const defaultConfig = createConfig({
version: await getTailwindVersion(),
- };
+ });
const writeTimeout = 10;
try {
@@ -102,19 +84,6 @@ export async function setupConfig(): Promise {
setTimeout(() => fs.writeFile(configFilePath, JSON.stringify(newConfig, null, 2)), writeTimeout);
}
- if (
- newConfig.dark !== defaultConfig.dark ||
- newConfig.prefix !== defaultConfig.prefix ||
- newConfig.version !== defaultConfig.version
- ) {
- // TODO: search for in the project and warn if it's not found
- console.info(
- `\n[!] Custom values detected in ${configFilePath}, render at root level of your app to sync runtime with node config values.`,
- `\n[!] Otherwise, your app will use the default values instead of your custom configuration.`,
- `\n[!] Example: In case of custom 'prefix' or 'version', the app will not display the correct class names.`,
- );
- }
-
return newConfig;
} catch (error) {
if (error instanceof Error && error.message.includes("ENOENT")) {
diff --git a/packages/ui/src/cli/commands/setup-init.ts b/packages/ui/src/cli/commands/setup-init.ts
index 50ce162d1..a4d6ac903 100644
--- a/packages/ui/src/cli/commands/setup-init.ts
+++ b/packages/ui/src/cli/commands/setup-init.ts
@@ -3,7 +3,7 @@ import type { namedTypes } from "ast-types";
import { parse } from "recast";
import { initFilePath, initJsxFilePath } from "../consts";
import { compareNodes } from "../utils/compare-nodes";
-import type { Config } from "./setup-config";
+import type { Config } from "../utils/create-config";
/**
* Sets up the `.flowbite-react/init.tsx` file in the project.
diff --git a/packages/ui/src/cli/utils/add-import.test.ts b/packages/ui/src/cli/utils/add-import.test.ts
index 8e3b12db5..abe2f29f2 100644
--- a/packages/ui/src/cli/utils/add-import.test.ts
+++ b/packages/ui/src/cli/utils/add-import.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it } from "vitest";
+import { describe, expect, it } from "bun:test";
import { addImport } from "./add-import";
describe("addImport", () => {
diff --git a/packages/ui/src/cli/utils/add-plugin.test.ts b/packages/ui/src/cli/utils/add-plugin.test.ts
index 3d3188548..6a7cbc46f 100644
--- a/packages/ui/src/cli/utils/add-plugin.test.ts
+++ b/packages/ui/src/cli/utils/add-plugin.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it } from "vitest";
+import { describe, expect, it } from "bun:test";
import { addPlugin } from "./add-plugin";
describe("addPlugin", () => {
diff --git a/packages/ui/src/cli/utils/add-to-config.test.ts b/packages/ui/src/cli/utils/add-to-config.test.ts
index 53df5f2f7..777234359 100644
--- a/packages/ui/src/cli/utils/add-to-config.test.ts
+++ b/packages/ui/src/cli/utils/add-to-config.test.ts
@@ -1,5 +1,5 @@
+import { describe, expect, it } from "bun:test";
import * as recast from "recast";
-import { describe, expect, it } from "vitest";
import { addToConfig } from "./add-to-config";
describe("addToConfig", () => {
diff --git a/packages/ui/src/cli/utils/compare-nodes.test.ts b/packages/ui/src/cli/utils/compare-nodes.test.ts
index da94535da..4aec00a81 100644
--- a/packages/ui/src/cli/utils/compare-nodes.test.ts
+++ b/packages/ui/src/cli/utils/compare-nodes.test.ts
@@ -1,5 +1,5 @@
+import { describe, expect, it } from "bun:test";
import { parse } from "recast";
-import { describe, expect, it } from "vitest";
import { compareNodes } from "./compare-nodes";
describe("compareNodes", () => {
diff --git a/packages/ui/src/cli/utils/create-config.ts b/packages/ui/src/cli/utils/create-config.ts
new file mode 100644
index 000000000..d8085841d
--- /dev/null
+++ b/packages/ui/src/cli/utils/create-config.ts
@@ -0,0 +1,24 @@
+export interface Config {
+ $schema: string;
+ components: string[];
+ dark: boolean;
+ path: string;
+ prefix: string;
+ rsc: boolean;
+ tsx: boolean;
+ version: 3 | 4;
+}
+
+export function createConfig(input: Partial = {}): Config {
+ return {
+ $schema: input.$schema ?? "https://unpkg.com/flowbite-react/schema.json",
+ components: input.components ?? [],
+ dark: input.dark ?? true,
+ path: input.path ?? "src/components",
+ // TODO: infer from project
+ prefix: input.prefix ?? "",
+ rsc: input.rsc ?? true,
+ tsx: input.tsx ?? true,
+ version: input.version ?? 4,
+ };
+}
diff --git a/packages/ui/src/cli/utils/create-init-logger.test.ts b/packages/ui/src/cli/utils/create-init-logger.test.ts
new file mode 100644
index 000000000..0486d70bc
--- /dev/null
+++ b/packages/ui/src/cli/utils/create-init-logger.test.ts
@@ -0,0 +1,152 @@
+import { describe, expect, it } from "bun:test";
+import { hasThemeInit } from "./create-init-logger";
+
+describe("hasThemeInit", () => {
+ it("should detect self-closing ThemeInit with space", () => {
+ expect(hasThemeInit("")).toBe(true);
+ });
+
+ it("should detect self-closing ThemeInit without space", () => {
+ expect(hasThemeInit("")).toBe(true);
+ });
+
+ it("should detect empty ThemeInit tags", () => {
+ expect(hasThemeInit("")).toBe(true);
+ });
+
+ it("should not detect ThemeInit with content", () => {
+ expect(hasThemeInit("some content")).toBe(false);
+ });
+
+ it("should not detect partial matches", () => {
+ expect(hasThemeInit("ThemeInit")).toBe(false);
+ });
+
+ it("should handle whitespace variations", () => {
+ expect(hasThemeInit("")).toBe(true);
+ expect(hasThemeInit("")).toBe(true);
+ });
+
+ it("should return false for empty content", () => {
+ expect(hasThemeInit("")).toBe(false);
+ });
+
+ it("should handle commented out ThemeInit", () => {
+ expect(hasThemeInit("")).toBe(false);
+ expect(hasThemeInit("{/* */}")).toBe(false);
+ expect(hasThemeInit("// ")).toBe(false);
+ });
+
+ it("should detect ThemeInit in JSX with children", () => {
+ const content = `
+ import type { PropsWithChildren } from "react";
+ import { ThemeInit } from "../.flowbite-react/init";
+
+ export default function RootLayout({ children }: PropsWithChildren) {
+ return (
+
+
+
+ {children}
+
+
+ );
+ }
+ `;
+ expect(hasThemeInit(content)).toBe(true);
+ });
+
+ it("should detect ThemeInit in JSX", () => {
+ const content = `
+ import { ThemeInit } from "../.flowbite-react/init";
+ import { App } from "./App";
+
+ export default function App() {
+ return (
+ <>
+
+
+ >
+ );
+ }
+ `;
+ expect(hasThemeInit(content)).toBe(true);
+ });
+
+ it("should not detect ThemeInit in JSX", () => {
+ const content = `
+ import { ThemeInit } from "../.flowbite-react/init";
+ import { App } from "./App";
+
+ export default function App() {
+ return (
+ <>
+
+ >
+ );
+ }
+ `;
+ expect(hasThemeInit(content)).toBe(false);
+ });
+
+ it("should detect multiple ThemeInit components", () => {
+ const content = `
+
+ Some content
+
+ `;
+ expect(hasThemeInit(content)).toBe(true);
+ });
+
+ it("should detect ThemeInit with attributes/props", () => {
+ expect(hasThemeInit('')).toBe(true);
+ expect(hasThemeInit('')).toBe(true);
+ expect(hasThemeInit('')).toBe(true);
+ });
+
+ it("should detect ThemeInit with newlines between tags", () => {
+ expect(
+ hasThemeInit(``),
+ ).toBe(true);
+ expect(
+ hasThemeInit(``),
+ ).toBe(true);
+ });
+
+ it("should handle more comment variations", () => {
+ expect(hasThemeInit("/* */")).toBe(false);
+ expect(
+ hasThemeInit(`/**
+ *
+ */`),
+ ).toBe(false);
+ expect(hasThemeInit("# ")).toBe(false);
+ });
+
+ it("should not detect case variations of ThemeInit", () => {
+ expect(hasThemeInit("")).toBe(false);
+ expect(hasThemeInit("")).toBe(false);
+ expect(hasThemeInit("")).toBe(false);
+ });
+
+ it("should detect ThemeInit in template literals", () => {
+ const content = `
+ const template = \`
+
+
+
+ \`;
+ `;
+ expect(hasThemeInit(content)).toBe(true);
+ });
+
+ it("should not detect malformed ThemeInit tags", () => {
+ expect(hasThemeInit("< ThemeInit/>")).toBe(false);
+ expect(hasThemeInit("")).toBe(false);
+ expect(hasThemeInit("")).toBe(false);
+ expect(hasThemeInit("<")).toBe(false);
+ });
+});
diff --git a/packages/ui/src/cli/utils/create-init-logger.ts b/packages/ui/src/cli/utils/create-init-logger.ts
new file mode 100644
index 000000000..0b92fb51d
--- /dev/null
+++ b/packages/ui/src/cli/utils/create-init-logger.ts
@@ -0,0 +1,74 @@
+import { configFilePath, initFilePath, initJsxFilePath } from "../consts";
+import { createConfig, type Config } from "./create-config";
+
+/**
+ * Creates a logger to track and warn about `` component usage.
+ *
+ * @param {Config} config - The configuration object used to check
+ */
+export function createInitLogger(config: Config) {
+ const defaultConfig = createConfig();
+
+ return {
+ config,
+ checkedMap: new Map(),
+ get isCustomConfig() {
+ return (
+ this.config.dark !== defaultConfig.dark ||
+ this.config.prefix !== defaultConfig.prefix ||
+ this.config.version !== defaultConfig.version
+ );
+ },
+ get showWarning() {
+ return this.checkedMap.values().find((value) => value) === undefined;
+ },
+ /**
+ * Checks if `` component is used in the given file content
+ *
+ * @param path - The path to the file being checked
+ * @param content - The file content to search in
+ */
+ check(path: string, content: string) {
+ if (this.isCustomConfig) {
+ this.checkedMap.set(path, hasThemeInit(content));
+ }
+ },
+ /**
+ * Logs a warning if `` component is not used in the project and the configuration `dark`, `prefix` or `version` differs from default values.
+ */
+ log() {
+ if (this.isCustomConfig && this.showWarning) {
+ console.warn(
+ `\n[!] Custom values detected in ${configFilePath}, render '' from ${config.tsx ? initFilePath : initJsxFilePath} at root level of your app to sync runtime with node config values.`,
+ `\n[!] Otherwise, your app will use the default values instead of your custom configuration.`,
+ `\n[!] Example: In case of custom 'prefix' or 'version', the app will not display the correct class names.`,
+ );
+ }
+ },
+ };
+}
+
+/**
+ * Checks if `` component is used in the given file content
+ *
+ * @param content - The file content to search in
+ * @returns boolean indicating if ThemeInit is used
+ */
+export function hasThemeInit(content: string): boolean {
+ // First check for commented out ThemeInit
+ if (/(\/\/|