From 025b97446454626ec9b7b8b5065142a7b3bcc854 Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Sun, 24 May 2026 23:01:08 +0300 Subject: [PATCH 1/2] fix(cli): clarify invalid typegen system exports (#10825) Co-authored-by: cyphercodes --- .../fix-cli-typegen-system-export-error.md | 7 ++ packages/cli/__tests__/io.test.ts | 75 +++++++++++++++++++ packages/cli/src/utils/io.ts | 68 +++++++++++++---- 3 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 .changeset/fix-cli-typegen-system-export-error.md create mode 100644 packages/cli/__tests__/io.test.ts diff --git a/.changeset/fix-cli-typegen-system-export-error.md b/.changeset/fix-cli-typegen-system-export-error.md new file mode 100644 index 00000000000..d8f60d5917d --- /dev/null +++ b/.changeset/fix-cli-typegen-system-export-error.md @@ -0,0 +1,7 @@ +--- +"@chakra-ui/cli": patch +--- + +Improve the `chakra typegen` error when the input file does not export a Chakra +system, including the discovered exports and a `createSystem(...)` example for +files that export `defineConfig(...)` configs. diff --git a/packages/cli/__tests__/io.test.ts b/packages/cli/__tests__/io.test.ts new file mode 100644 index 00000000000..207ca07444d --- /dev/null +++ b/packages/cli/__tests__/io.test.ts @@ -0,0 +1,75 @@ +import { mkdirSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { TextEncoder } from "node:util" +import { afterEach, beforeEach, describe, expect, it } from "vitest" + +const nodeUint8Array = new TextEncoder().encode("").constructor + +const readInput = async (file: string, cwd: string) => { + Object.defineProperties(globalThis, { + TextEncoder: { + configurable: true, + value: TextEncoder, + }, + Uint8Array: { + configurable: true, + value: nodeUint8Array, + }, + }) + + const { read } = await import("../src/utils/io") + return read(file, { cwd }) +} + +describe("io.read", () => { + let testDir: string + + beforeEach(() => { + testDir = join(tmpdir(), `chakra-cli-io-test-${Date.now()}`) + mkdirSync(testDir, { recursive: true }) + }) + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }) + }) + + it("accepts a default Chakra system export", async () => { + const source = join(testDir, "system.ts") + writeFileSync(source, "export default { $$chakra: true }") + + const result = await readInput(source, testDir) + + expect(result.mod).toMatchObject({ $$chakra: true }) + }) + + it("explains when the default export is not a Chakra system", async () => { + const source = join(testDir, "config.ts") + writeFileSync(source, "export default { theme: { tokens: {} } }") + + await expect(readInput(source, testDir)).rejects.toThrow( + /No Chakra system export found/, + ) + await expect(readInput(source, testDir)).rejects.toThrow( + 'Found export: "default".', + ) + await expect(readInput(source, testDir)).rejects.toThrow( + "defineConfig(...)", + ) + await expect(readInput(source, testDir)).rejects.toThrow( + "createSystem(defaultConfig, config)", + ) + }) + + it("lists named exports when no Chakra system export is found", async () => { + const source = join(testDir, "theme.ts") + writeFileSync(source, "export const theme = { tokens: {} }") + + await expect(readInput(source, testDir)).rejects.toThrow( + 'Found export: "theme".', + ) + await expect(readInput(source, testDir)).rejects.toThrow( + 'expects "default", "preset", "system"', + ) + }) +}) diff --git a/packages/cli/src/utils/io.ts b/packages/cli/src/utils/io.ts index 2152c5c7a7e..59d7fddaf8b 100644 --- a/packages/cli/src/utils/io.ts +++ b/packages/cli/src/utils/io.ts @@ -73,12 +73,12 @@ function loadViaRequire(file: string, code: string): any { } } - delete require.cache[require.resolve(file)] - const raw = require(file) - const result = raw.default ?? raw - - require.extensions[ext] = defaultLoader - return result + try { + delete require.cache[require.resolve(file)] + return require(file) + } finally { + require.extensions[ext] = defaultLoader + } } function loadViaVm(code: string): any { @@ -89,12 +89,55 @@ function loadViaVm(code: string): any { require, }) vm.runInContext(code, ctx) - const raw = mod.exports as any - return raw.default ?? raw + return mod.exports +} + +const systemExports = ["default", "preset", "system"] as const + +const isRecord = (mod: unknown): mod is Record => { + return typeof mod === "object" && mod !== null } const isValidSystem = (mod: unknown): mod is SystemContext => { - return Object.hasOwnProperty.call(mod, "$$chakra") + return isRecord(mod) && Object.hasOwnProperty.call(mod, "$$chakra") +} + +function resolveSystemExport(mod: unknown) { + const candidate = isRecord(mod) && mod.default != null ? mod.default : mod + + if (!isRecord(candidate) || isValidSystem(candidate)) return candidate + + const exports = candidate as Record + return exports.default || exports.preset || exports.system || candidate +} + +function formatExportName(name: string) { + return `"${name}"` +} + +function getExportNames(mod: unknown) { + if (!isRecord(mod) || isValidSystem(mod)) return [] + return Object.keys(mod).filter((name) => name !== "__esModule") +} + +function formatInvalidSystemError(file: string, mod: unknown) { + const exportNames = getExportNames(mod) + const foundExports = exportNames.length + ? `Found export${exportNames.length === 1 ? "" : "s"}: ${exportNames + .map(formatExportName) + .join(", ")}.` + : "Found no named exports." + + return [ + `No Chakra system export found in ${file}.`, + `The chakra typegen command expects ${systemExports + .map(formatExportName) + .join( + ", ", + )}, or a CommonJS export to be a Chakra system created with createSystem(...).`, + foundExports, + "If this file exports a config from defineConfig(...), wrap it first: export default createSystem(defaultConfig, config).", + ].join(" ") } export const read = async ( @@ -106,13 +149,10 @@ export const read = async ( const bundle = await bundleFile(filePath, cwd, tsconfig) const mod = loadBundledCode(filePath, bundle.code) - - const resolvedMod = mod.default || mod.preset || mod.system || mod + const resolvedMod = resolveSystemExport(mod) if (!isValidSystem(resolvedMod)) { - throw new Error( - `No default export found in ${file}. Did you forget to provide an export default?`, - ) + throw new Error(formatInvalidSystemError(file, mod)) } return { mod: resolvedMod, dependencies: bundle.dependencies } From 473936922b06542f47404c76760fbf22d339b9c1 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Sun, 24 May 2026 22:10:05 +0200 Subject: [PATCH 2/2] test(cli): improve typegen system export coverage --- packages/cli/__tests__/io.test.ts | 65 +++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/packages/cli/__tests__/io.test.ts b/packages/cli/__tests__/io.test.ts index 207ca07444d..52f36c7c534 100644 --- a/packages/cli/__tests__/io.test.ts +++ b/packages/cli/__tests__/io.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, rmSync, writeFileSync } from "node:fs" +import { mkdtempSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" import { TextEncoder } from "node:util" @@ -22,12 +22,22 @@ const readInput = async (file: string, cwd: string) => { return read(file, { cwd }) } +const getReadError = async (file: string, cwd: string) => { + try { + await readInput(file, cwd) + } catch (error) { + expect(error).toBeInstanceOf(Error) + return error as Error + } + + throw new Error("Expected io.read to throw") +} + describe("io.read", () => { let testDir: string beforeEach(() => { - testDir = join(tmpdir(), `chakra-cli-io-test-${Date.now()}`) - mkdirSync(testDir, { recursive: true }) + testDir = mkdtempSync(join(tmpdir(), "chakra-cli-io-test-")) }) afterEach(() => { @@ -43,33 +53,46 @@ describe("io.read", () => { expect(result.mod).toMatchObject({ $$chakra: true }) }) + it.each(["preset", "system"] as const)( + "accepts a named %s Chakra system export", + async (exportName) => { + const source = join(testDir, `${exportName}.ts`) + writeFileSync(source, `export const ${exportName} = { $$chakra: true }`) + + const result = await readInput(source, testDir) + + expect(result.mod).toMatchObject({ $$chakra: true }) + }, + ) + + it("accepts a CommonJS Chakra system export", async () => { + const source = join(testDir, "system.cjs") + writeFileSync(source, "module.exports = { $$chakra: true }") + + const result = await readInput(source, testDir) + + expect(result.mod).toMatchObject({ $$chakra: true }) + }) + it("explains when the default export is not a Chakra system", async () => { const source = join(testDir, "config.ts") writeFileSync(source, "export default { theme: { tokens: {} } }") - await expect(readInput(source, testDir)).rejects.toThrow( - /No Chakra system export found/, - ) - await expect(readInput(source, testDir)).rejects.toThrow( - 'Found export: "default".', - ) - await expect(readInput(source, testDir)).rejects.toThrow( - "defineConfig(...)", - ) - await expect(readInput(source, testDir)).rejects.toThrow( - "createSystem(defaultConfig, config)", - ) + const error = await getReadError(source, testDir) + + expect(error.message).toContain("No Chakra system export found") + expect(error.message).toContain('Found export: "default".') + expect(error.message).toContain("defineConfig(...)") + expect(error.message).toContain("createSystem(defaultConfig, config)") }) it("lists named exports when no Chakra system export is found", async () => { const source = join(testDir, "theme.ts") writeFileSync(source, "export const theme = { tokens: {} }") - await expect(readInput(source, testDir)).rejects.toThrow( - 'Found export: "theme".', - ) - await expect(readInput(source, testDir)).rejects.toThrow( - 'expects "default", "preset", "system"', - ) + const error = await getReadError(source, testDir) + + expect(error.message).toContain('Found export: "theme".') + expect(error.message).toContain('expects "default", "preset", "system"') }) })