shadcn-ui/ui · error · Error
Invalid input: not an object literal
Error message
Invalid input: not an object literal
What it means
Thrown by shadcn's tailwind-config updater (ts-morph based) when it cannot find an object literal to mutate. The updater grabs sourceFile.getStatements()[0], expects a VariableStatement whose first declaration's initializer is an ObjectLiteralExpression, and throws if either check fails. It is a hard precondition: shadcn can only patch a config that is a plain exported object literal.
Source
Thrown at packages/shadcn/src/utils/updaters/update-tailwind-config.ts:433
async function parseObjectLiteral(objectLiteralString: string): Promise<any> {
const sourceFile = await _createSourceFile(
`const theme = ${objectLiteralString}`,
null
)
const statement = sourceFile.getStatements()[0]
if (statement?.getKind() === SyntaxKind.VariableStatement) {
const declaration = (statement as VariableStatement)
.getDeclarationList()
?.getDeclarations()[0]
const initializer = declaration.getInitializer()
if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
return await parseObjectLiteralExpression(initializer)
}
}
throw new Error("Invalid input: not an object literal")
}
function parseObjectLiteralExpression(node: ObjectLiteralExpression): any {
const result: any = {}
for (const property of node.getProperties()) {
if (property.isKind(SyntaxKind.PropertyAssignment)) {
const name = property.getName().replace(/\'/g, "")
if (
property.getInitializer()?.isKind(SyntaxKind.ObjectLiteralExpression)
) {
result[name] = parseObjectLiteralExpression(
property.getInitializer() as ObjectLiteralExpression
)
} else if (
property.getInitializer()?.isKind(SyntaxKind.ArrayLiteralExpression)
) {
result[name] = parseArrayLiteralExpression(
property.getInitializer() as ArrayLiteralExpressionView on GitHub (pinned to efac598707)
Solutions
- Rewrite the config so its first top-level statement is a variable/export holding a literal object: `const config = { content: [...], theme: {...} }; export default config` (or `export default { ... }` / `module.exports = { ... }`). No function wrappers, no ternaries, no merge() calls around the object.
- Remove any `import`/comment-only or directive first lines so getStatements()[0] is the config variable statement, and move imports above it only if they remain statements before it that are NOT the config (ts-morph indexes the literal first statement, so the config must be statement [0]).
- If you need a merged/derived config, build it as a plain object literal first and only assign/merge INTO that literal, then export the literal; do not export the result of a helper call.
- Pin/upgrade shadcn to a version matching your Tailwind major (v3 vs v4) so the updater targets the correct file shape.
Example fix
// before
const config = merge(baseConfig, {
theme: { extend: { colors: {} } }
})
export default config
// after
const config = {
...baseConfig,
theme: { extend: { colors: {} } }
}
export default config Defensive patterns
Strategy: validation
Validate before calling
// Before calling the shadcn updater, sanity-check the config shape with ts-morph:
import { Project, SyntaxKind } from "ts-morph"
const sf = new Project().addSourceFileAtPath("tailwind.config.ts")
const stmt = sf.getStatements()[0]
const decl = stmt?.getKind() === SyntaxKind.VariableStatement
? stmt.getFirstChildByKind(SyntaxKind.VariableDeclarationList)
?.getDeclarations()[0]
: undefined
const ok = decl?.getInitializer()?.isKind(SyntaxKind.ObjectLiteralExpression) ?? false
if (!ok) throw new Error("tailwind config is not a plain object literal; shadcn cannot patch it") Type guard
// Narrow an exported config node to a patchable object literal.
import { Node, ObjectLiteralExpression, SyntaxKind } from "ts-morph"
function isPatchableObjectLiteral(node: Node | undefined): node is ObjectLiteralExpression {
return node?.isKind(SyntaxKind.ObjectLiteralExpression) ?? false
} Try / catch
try {
await updateTailwindConfig(...) // shadcn call
} catch (e) {
if (String(e?.message).includes("not an object literal")) {
throw new Error("tailwind.config must export a plain object literal; rewrite merge()/function wrappers before re-running shadcn.")
}
throw e
} Prevention
- Keep tailwind.config as a single exported object literal — no function bodies, no helper-wrapped exports.
- Avoid `module.exports = merge(...)` or `config({...})` wrappers around the exported config.
- Run `shadcn init` against a freshly generated Tailwind config so the shape matches the updater's expectation.
- For Tailwind v4 (CSS-first), confirm shadcn version supports the v4 config model before running add/init.
When it happens
Trigger: Calling `shadcn` (init/add) against a tailwind.config.{js,ts,mjs,cjs} whose first statement is NOT `const config = { ... }` / `export default { ... }` / `module.exports = { ... }`. Triggered when the config exports a function (e.g. `export default function() { return {...} }`), a conditional/ternary, a Promise, a merge like `module.exports = merge(...)`, an empty file, or a file whose first statement is an import/pragma rather than the config variable.
Common situations: Tailwind v4 setups that use a `@theme` CSS-first config and leave the JS config minimal or function-shaped; configs wrapped in `config({ ... })` helpers (e.g. from `@tailwindcss/typography` or custom wrappers); configs generated by other tools that export a function; monorepos where shadcn picks the wrong config path.
Related errors
- Failed to read config at ${options.cwd}.
- Invalid base: ${String(resolvedBase)}. Expected one of: ${PR
- Could not resolve shadcn/tailwind.css.
- Unknown client: ${client}. Available clients: ${CLIENTS.map(
- No `components.json` file found. Ensure you are at the root
AI-assisted analysis of shadcn-ui/ui@efac598707 (2026-08-12).
Data as JSON: /api/errors/5638161c9f200fb1.
Report an issue: GitHub.