mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
fix
This commit is contained in:
@@ -129,13 +129,13 @@ function buildGlobalsCss(registryBase: RegistryItem) {
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -74,10 +74,13 @@ Add the following to your styles/globals.css file. You can learn more about usin
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
|
||||
@@ -199,6 +199,7 @@ export const create = new Command()
|
||||
initUrl = resolveInitUrl(
|
||||
{
|
||||
...decoded,
|
||||
base: "radix",
|
||||
rtl: opts.rtl ?? false,
|
||||
},
|
||||
{ template }
|
||||
|
||||
@@ -58,12 +58,14 @@ export const initOptionsSchema = z.object({
|
||||
yes: z.boolean(),
|
||||
defaults: z.boolean(),
|
||||
force: z.boolean(),
|
||||
reinstall: z.boolean(),
|
||||
reinstall: z.boolean().optional(),
|
||||
silent: z.boolean(),
|
||||
isNewProject: z.boolean().default(false),
|
||||
cssVariables: z.boolean().default(true),
|
||||
rtl: z.boolean().optional(),
|
||||
base: z.enum(["radix", "base"]).optional(),
|
||||
template: z.string().optional(),
|
||||
existingConfig: z.record(z.unknown()).optional(),
|
||||
installStyleIndex: z.boolean().default(true),
|
||||
registryBaseConfig: rawConfigSchema.deepPartial().optional(),
|
||||
})
|
||||
@@ -77,6 +79,7 @@ export const init = new Command()
|
||||
"-t, --template <template>",
|
||||
"the template to use. (next, start, vite, next-monorepo, react-router)"
|
||||
)
|
||||
.option("-b, --base <base>", "the primitive library to use. (radix, base)")
|
||||
.option("-p, --preset [name]", "use a preset configuration")
|
||||
.option("-y, --yes", "skip confirmation prompt.", true)
|
||||
.option(
|
||||
@@ -96,14 +99,26 @@ export const init = new Command()
|
||||
.option("--no-css-variables", "do not use css variables for theming.")
|
||||
.option("--rtl", "enable RTL support.")
|
||||
.option("--no-rtl", "disable RTL support.")
|
||||
.option("--reinstall", "re-install existing UI components.", false)
|
||||
.option("--reinstall", "re-install existing UI components.")
|
||||
.option("--no-reinstall", "do not re-install existing UI components.")
|
||||
.action(async (components, opts) => {
|
||||
let componentsJsonBackupPath: string | undefined
|
||||
let reinstallComponents: string[] = []
|
||||
|
||||
// Restore components.json backup on unexpected exit (e.g. process.exit in preflight).
|
||||
const restoreBackupOnExit = () => {
|
||||
if (componentsJsonBackupPath) {
|
||||
restoreFileBackup(
|
||||
componentsJsonBackupPath.replace(FILE_BACKUP_SUFFIX, "")
|
||||
)
|
||||
}
|
||||
}
|
||||
process.on("exit", restoreBackupOnExit)
|
||||
|
||||
try {
|
||||
const options = initOptionsSchema.parse({
|
||||
...opts,
|
||||
reinstall: opts.reinstall,
|
||||
cwd: path.resolve(opts.cwd),
|
||||
})
|
||||
const presets = Object.values(DEFAULT_PRESETS)
|
||||
@@ -111,18 +126,11 @@ export const init = new Command()
|
||||
presets.map((preset) => [preset.name, preset])
|
||||
)
|
||||
|
||||
let newBase: string | undefined
|
||||
let presetBase: string | undefined
|
||||
|
||||
if (options.defaults) {
|
||||
options.template = options.template || "next"
|
||||
const initUrl = resolveInitUrl(
|
||||
{
|
||||
...DEFAULT_PRESETS["base-nova"],
|
||||
rtl: options.rtl ?? false,
|
||||
},
|
||||
{ template: options.template }
|
||||
)
|
||||
components = [initUrl, ...components]
|
||||
newBase = DEFAULT_PRESETS["base-nova"].base
|
||||
// Base resolution happens below — just mark presetBase as undefined.
|
||||
}
|
||||
|
||||
if (options.template && !(options.template in templates)) {
|
||||
@@ -193,6 +201,11 @@ export const init = new Command()
|
||||
// Ignore read errors.
|
||||
}
|
||||
|
||||
// Pass existing config so preflight can use it (e.g. tailwind.css path in monorepos).
|
||||
if (existingConfig) {
|
||||
options.existingConfig = existingConfig
|
||||
}
|
||||
|
||||
let shouldReinstall = options.reinstall
|
||||
|
||||
if (!shouldReinstall) {
|
||||
@@ -275,7 +288,7 @@ export const init = new Command()
|
||||
template: options.template,
|
||||
})
|
||||
components = [result.url, ...components]
|
||||
newBase = result.base
|
||||
presetBase = result.base
|
||||
}
|
||||
|
||||
if (typeof presetArg === "string") {
|
||||
@@ -289,7 +302,7 @@ export const init = new Command()
|
||||
url.searchParams.delete("rtl")
|
||||
}
|
||||
initUrl = url.toString()
|
||||
newBase = url.searchParams.get("base") ?? undefined
|
||||
presetBase = url.searchParams.get("base") ?? undefined
|
||||
} else if (isPresetCode(presetArg)) {
|
||||
const decoded = decodePreset(presetArg)
|
||||
if (!decoded) {
|
||||
@@ -299,14 +312,17 @@ export const init = new Command()
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
// Preset codes no longer carry base — use "radix" as placeholder.
|
||||
// The correct base is set in the URL after resolution below.
|
||||
initUrl = resolveInitUrl(
|
||||
{
|
||||
...decoded,
|
||||
base: "radix",
|
||||
rtl: options.rtl ?? false,
|
||||
},
|
||||
{ template: options.template }
|
||||
)
|
||||
newBase = decoded.base
|
||||
presetBase = undefined
|
||||
} else {
|
||||
const preset = presetsByName.get(presetArg)!
|
||||
initUrl = resolveInitUrl(
|
||||
@@ -316,23 +332,80 @@ export const init = new Command()
|
||||
},
|
||||
{ template: options.template }
|
||||
)
|
||||
newBase = preset.base
|
||||
presetBase = preset.base
|
||||
}
|
||||
|
||||
components = [initUrl, ...components]
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve base: --base flag > preset/prompt/URL > existing config > prompt.
|
||||
let resolvedBase: string =
|
||||
options.base ??
|
||||
presetBase ??
|
||||
(existingConfig?.style
|
||||
? (existingConfig.style as string).startsWith("base-")
|
||||
? "base"
|
||||
: "radix"
|
||||
: "")
|
||||
|
||||
if (!resolvedBase) {
|
||||
const { base } = await prompts({
|
||||
type: "select",
|
||||
name: "base",
|
||||
message: `Which ${highlighter.info(
|
||||
"primitive library"
|
||||
)} would you like to use?`,
|
||||
choices: [
|
||||
{ title: "Radix", value: "radix" },
|
||||
{ title: "Base", value: "base" },
|
||||
],
|
||||
})
|
||||
if (!base) process.exit(0)
|
||||
resolvedBase = base
|
||||
}
|
||||
|
||||
// Build the --defaults URL now that base is resolved.
|
||||
if (options.defaults && components.length === 0) {
|
||||
const presetName = resolvedBase === "base" ? "base-nova" : "radix-nova"
|
||||
const initUrl = resolveInitUrl(
|
||||
{
|
||||
...DEFAULT_PRESETS[presetName],
|
||||
rtl: options.rtl ?? false,
|
||||
},
|
||||
{ template: options.template }
|
||||
)
|
||||
components = [initUrl, ...components]
|
||||
}
|
||||
|
||||
// Ensure the init URL has the correct base.
|
||||
if (components.length > 0 && isUrl(components[0])) {
|
||||
const url = new URL(components[0])
|
||||
url.searchParams.set("base", resolvedBase)
|
||||
components[0] = url.toString()
|
||||
}
|
||||
|
||||
// Confirm if the user is switching bases during reinit.
|
||||
if (existingConfig?.style) {
|
||||
const confirmedBase = await confirmBaseSwitch(
|
||||
existingConfig.style as string,
|
||||
resolvedBase
|
||||
)
|
||||
if (confirmedBase !== resolvedBase) {
|
||||
resolvedBase = confirmedBase
|
||||
if (components.length > 0 && isUrl(components[0])) {
|
||||
const url = new URL(components[0])
|
||||
url.searchParams.set("base", confirmedBase)
|
||||
components[0] = url.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add re-install components after preset selection.
|
||||
if (reinstallComponents.length) {
|
||||
components = [...components, ...reinstallComponents]
|
||||
}
|
||||
|
||||
// Warn if the user is switching bases during reinit.
|
||||
if (existingConfig?.style && newBase) {
|
||||
warnOnBaseSwitch(existingConfig.style as string, newBase)
|
||||
}
|
||||
|
||||
options.components = components
|
||||
|
||||
await loadEnvFiles(options.cwd)
|
||||
@@ -378,15 +451,14 @@ export const init = new Command()
|
||||
`Project initialization completed.\nYou may now add components.`
|
||||
)
|
||||
|
||||
// We need when running with custom cwd.
|
||||
// Success — remove the backup and exit listener.
|
||||
process.removeListener("exit", restoreBackupOnExit)
|
||||
deleteFileBackup(path.resolve(cwd, "components.json"))
|
||||
logger.break()
|
||||
} catch (error) {
|
||||
if (componentsJsonBackupPath) {
|
||||
restoreFileBackup(
|
||||
componentsJsonBackupPath.replace(FILE_BACKUP_SUFFIX, "")
|
||||
)
|
||||
}
|
||||
// Restore handled by exit listener, but also do it here for non-exit errors.
|
||||
process.removeListener("exit", restoreBackupOnExit)
|
||||
restoreBackupOnExit()
|
||||
logger.break()
|
||||
handleError(error)
|
||||
} finally {
|
||||
@@ -721,22 +793,31 @@ async function promptForMinimalConfig(
|
||||
})
|
||||
}
|
||||
|
||||
function warnOnBaseSwitch(existingStyle: string, newBase: string) {
|
||||
async function confirmBaseSwitch(existingStyle: string, resolvedBase: string) {
|
||||
// Styles prefixed with "base-" use Base UI. Everything else is Radix.
|
||||
const oldBase = existingStyle.startsWith("base-") ? "base" : "radix"
|
||||
if (newBase !== oldBase) {
|
||||
logger.warn(
|
||||
` You are switching from ${highlighter.info(
|
||||
oldBase
|
||||
)} to ${highlighter.info(newBase)}.`
|
||||
)
|
||||
logger.warn(
|
||||
` Components outside the ${highlighter.info(
|
||||
"ui"
|
||||
)} directory that depend on ${highlighter.info(
|
||||
oldBase
|
||||
)} primitives may need manual updates.`
|
||||
)
|
||||
logger.break()
|
||||
}
|
||||
if (resolvedBase === oldBase) return resolvedBase
|
||||
|
||||
logger.warn(
|
||||
` You are switching from ${highlighter.info(
|
||||
oldBase
|
||||
)} to ${highlighter.info(resolvedBase)}.`
|
||||
)
|
||||
logger.warn(
|
||||
` Components outside the ${highlighter.info(
|
||||
"ui"
|
||||
)} directory that depend on ${highlighter.info(
|
||||
oldBase
|
||||
)} primitives may need manual updates.`
|
||||
)
|
||||
logger.break()
|
||||
|
||||
const { proceed } = await prompts({
|
||||
type: "confirm",
|
||||
name: "proceed",
|
||||
message: "Would you like to continue?",
|
||||
initial: false,
|
||||
})
|
||||
|
||||
return proceed ? resolvedBase : oldBase
|
||||
}
|
||||
|
||||
@@ -54,7 +54,13 @@ export async function preFlightInit(
|
||||
const frameworkSpinner = spinner(`Verifying framework.`, {
|
||||
silent: options.silent,
|
||||
}).start()
|
||||
const projectInfo = await getProjectInfo(options.cwd)
|
||||
const tailwind = options.existingConfig?.tailwind as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
const projectInfo = await getProjectInfo(options.cwd, {
|
||||
configCssFile:
|
||||
typeof tailwind?.css === "string" ? tailwind.css : undefined,
|
||||
})
|
||||
if (!projectInfo || projectInfo?.framework.name === "manual") {
|
||||
errors[ERRORS.UNSUPPORTED_FRAMEWORK] = true
|
||||
frameworkSpinner?.fail()
|
||||
|
||||
@@ -20,7 +20,6 @@ import { isSafeTarget } from "@/src/utils/is-safe-target"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import { updateCss } from "@/src/utils/updaters/update-css"
|
||||
import { updateCssVars } from "@/src/utils/updaters/update-css-vars"
|
||||
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
|
||||
import { updateEnvVars } from "@/src/utils/updaters/update-env-vars"
|
||||
import { updateFiles } from "@/src/utils/updaters/update-files"
|
||||
@@ -110,35 +109,19 @@ async function addProjectComponents(
|
||||
tree = await massageTreeForFonts(tree, config)
|
||||
}
|
||||
|
||||
await updateDependencies(tree.dependencies, tree.devDependencies, config, {
|
||||
silent: options.silent,
|
||||
})
|
||||
|
||||
await updateTailwindConfig(tree.tailwind?.config, config, {
|
||||
silent: options.silent,
|
||||
tailwindVersion,
|
||||
})
|
||||
|
||||
const overwriteCssVars =
|
||||
options.overwriteCssVars ??
|
||||
(await shouldOverwriteCssVars(components, config))
|
||||
await updateCssVars(tree.cssVars, config, {
|
||||
cleanupDefaultNextStyles: options.isNewProject,
|
||||
silent: options.silent,
|
||||
tailwindVersion,
|
||||
tailwindConfig: tree.tailwind?.config,
|
||||
overwriteCssVars,
|
||||
})
|
||||
|
||||
// Add CSS updater
|
||||
await updateCss(tree.css, config, {
|
||||
silent: options.silent,
|
||||
})
|
||||
|
||||
await updateEnvVars(tree.envVars, config, {
|
||||
silent: options.silent,
|
||||
})
|
||||
|
||||
await updateDependencies(tree.dependencies, tree.devDependencies, config, {
|
||||
silent: options.silent,
|
||||
})
|
||||
|
||||
if (!options.skipFonts) {
|
||||
await updateFonts(tree.fonts, config, {
|
||||
silent: options.silent,
|
||||
@@ -151,6 +134,21 @@ async function addProjectComponents(
|
||||
path: options.path,
|
||||
})
|
||||
|
||||
// Write CSS last so the file watcher triggers a rebuild
|
||||
// after all component files and dependencies are in place.
|
||||
const overwriteCssVars = tree.cssVars
|
||||
? (options.overwriteCssVars ??
|
||||
(await shouldOverwriteCssVars(components, config)))
|
||||
: undefined
|
||||
await updateCss(tree.css, config, {
|
||||
silent: options.silent,
|
||||
cssVars: tree.cssVars,
|
||||
cleanupDefaultNextStyles: options.isNewProject,
|
||||
overwriteCssVars,
|
||||
tailwindVersion,
|
||||
tailwindConfig: tree.tailwind?.config,
|
||||
})
|
||||
|
||||
if (tree.docs) {
|
||||
logger.info(tree.docs)
|
||||
}
|
||||
@@ -197,7 +195,7 @@ async function addWorkspaceComponents(
|
||||
|
||||
const rootSpinner = spinner(`Installing components.`)?.start()
|
||||
|
||||
// Process global updates (tailwind, css vars, dependencies) first for the main target.
|
||||
// Process global updates for the main target.
|
||||
// These should typically go to the UI package in a workspace.
|
||||
const mainTargetConfig = workspaceConfig.ui
|
||||
const tailwindVersion = await getProjectTailwindVersionFromConfig(
|
||||
@@ -208,7 +206,17 @@ async function addWorkspaceComponents(
|
||||
mainTargetConfig.resolvedPaths.ui
|
||||
)
|
||||
|
||||
// 1. Update tailwind config.
|
||||
// 1. Update dependencies.
|
||||
await updateDependencies(
|
||||
tree.dependencies,
|
||||
tree.devDependencies,
|
||||
mainTargetConfig,
|
||||
{
|
||||
silent: true,
|
||||
}
|
||||
)
|
||||
|
||||
// 2. Update tailwind config.
|
||||
if (tree.tailwind?.config) {
|
||||
await updateTailwindConfig(tree.tailwind?.config, mainTargetConfig, {
|
||||
silent: true,
|
||||
@@ -222,55 +230,21 @@ async function addWorkspaceComponents(
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Update css vars.
|
||||
if (tree.cssVars) {
|
||||
const overwriteCssVars =
|
||||
options.overwriteCssVars ??
|
||||
(await shouldOverwriteCssVars(components, config))
|
||||
await updateCssVars(tree.cssVars, mainTargetConfig, {
|
||||
silent: true,
|
||||
tailwindVersion,
|
||||
tailwindConfig: tree.tailwind?.config,
|
||||
overwriteCssVars,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, mainTargetConfig.resolvedPaths.tailwindCss)
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Update CSS
|
||||
if (tree.css) {
|
||||
await updateCss(tree.css, mainTargetConfig, {
|
||||
silent: true,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, mainTargetConfig.resolvedPaths.tailwindCss)
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Update environment variables
|
||||
// 3. Update environment variables.
|
||||
if (tree.envVars) {
|
||||
await updateEnvVars(tree.envVars, mainTargetConfig, {
|
||||
silent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 5. Update dependencies.
|
||||
await updateDependencies(
|
||||
tree.dependencies,
|
||||
tree.devDependencies,
|
||||
mainTargetConfig,
|
||||
{
|
||||
silent: true,
|
||||
}
|
||||
)
|
||||
|
||||
// 6. Update fonts.
|
||||
await updateFonts(tree.fonts, mainTargetConfig, {
|
||||
// 4. Update fonts.
|
||||
// Fonts modify the app's layout file (e.g. app/layout.tsx),
|
||||
// so we use the app config, not the UI workspace config.
|
||||
await updateFonts(tree.fonts, config, {
|
||||
silent: true,
|
||||
})
|
||||
|
||||
// 7. Group files by their type to determine target config and update files.
|
||||
// 5. Group files by their type to determine target config and update files.
|
||||
const filesByType = new Map<string, typeof tree.files>()
|
||||
|
||||
for (const file of tree.files ?? []) {
|
||||
@@ -281,11 +255,19 @@ async function addWorkspaceComponents(
|
||||
filesByType.get(type)!.push(file)
|
||||
}
|
||||
|
||||
const FILE_TYPE_TO_CONFIG_KEY: Record<string, string> = {
|
||||
"registry:ui": "ui",
|
||||
"registry:hook": "hooks",
|
||||
"registry:lib": "lib",
|
||||
}
|
||||
|
||||
// Process each type of component with its appropriate target config.
|
||||
for (const type of Array.from(filesByType.keys())) {
|
||||
const typeFiles = filesByType.get(type)!
|
||||
|
||||
let targetConfig = type === "registry:ui" ? workspaceConfig.ui : config
|
||||
const configKey = FILE_TYPE_TO_CONFIG_KEY[type]
|
||||
const targetConfig =
|
||||
configKey && workspaceConfig[configKey] ? workspaceConfig[configKey] : config
|
||||
|
||||
const typeWorkspaceRoot = findCommonRoot(
|
||||
config.resolvedPaths.cwd,
|
||||
@@ -324,6 +306,25 @@ async function addWorkspaceComponents(
|
||||
)
|
||||
}
|
||||
|
||||
// 6. Write CSS last so the file watcher triggers a rebuild
|
||||
// after all component files and dependencies are in place.
|
||||
const overwriteCssVars = tree.cssVars
|
||||
? (options.overwriteCssVars ??
|
||||
(await shouldOverwriteCssVars(components, config)))
|
||||
: undefined
|
||||
await updateCss(tree.css, mainTargetConfig, {
|
||||
silent: true,
|
||||
cssVars: tree.cssVars,
|
||||
overwriteCssVars,
|
||||
tailwindVersion,
|
||||
tailwindConfig: tree.tailwind?.config,
|
||||
})
|
||||
if (tree.cssVars || tree.css) {
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, mainTargetConfig.resolvedPaths.tailwindCss)
|
||||
)
|
||||
}
|
||||
|
||||
rootSpinner?.succeed()
|
||||
|
||||
// Deduplicate and sort files.
|
||||
|
||||
@@ -38,7 +38,10 @@ const TS_CONFIG_SCHEMA = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
||||
export async function getProjectInfo(
|
||||
cwd: string,
|
||||
opts?: { configCssFile?: string }
|
||||
): Promise<ProjectInfo | null> {
|
||||
const [
|
||||
configFiles,
|
||||
isSrcDir,
|
||||
@@ -60,7 +63,7 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
||||
fs.pathExists(path.resolve(cwd, "src")),
|
||||
isTypeScriptProject(cwd),
|
||||
getTailwindConfigFile(cwd),
|
||||
getTailwindCssFile(cwd),
|
||||
getTailwindCssFile(cwd, opts?.configCssFile),
|
||||
getTailwindVersion(cwd),
|
||||
getTsConfigAliasPrefix(cwd),
|
||||
getPackageInfo(cwd, false),
|
||||
@@ -242,7 +245,18 @@ export async function getTailwindVersion(
|
||||
return "v4"
|
||||
}
|
||||
|
||||
export async function getTailwindCssFile(cwd: string) {
|
||||
export async function getTailwindCssFile(
|
||||
cwd: string,
|
||||
configCssFile?: string
|
||||
) {
|
||||
// If the existing config has a known CSS file, check it first.
|
||||
if (configCssFile) {
|
||||
const resolvedPath = path.resolve(cwd, configCssFile)
|
||||
if (await fs.pathExists(resolvedPath)) {
|
||||
return configCssFile
|
||||
}
|
||||
}
|
||||
|
||||
const [files, tailwindVersion] = await Promise.all([
|
||||
fg.glob(["**/*.css", "**/*.scss"], {
|
||||
cwd,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
DEFAULT_PRESET_CONFIG,
|
||||
PRESET_BASES,
|
||||
PRESET_BASE_COLORS,
|
||||
PRESET_FONTS,
|
||||
PRESET_ICON_LIBRARIES,
|
||||
@@ -46,7 +45,6 @@ describe("encodePreset / decodePreset", () => {
|
||||
|
||||
it("should round-trip a custom config", () => {
|
||||
const config: PresetConfig = {
|
||||
base: "base",
|
||||
style: "lyra",
|
||||
baseColor: "zinc",
|
||||
theme: "blue",
|
||||
@@ -72,22 +70,19 @@ describe("encodePreset / decodePreset", () => {
|
||||
})
|
||||
|
||||
it("should handle partial config by filling defaults", () => {
|
||||
const code = encodePreset({ base: "base" })
|
||||
const code = encodePreset({ style: "lyra" })
|
||||
const decoded = decodePreset(code)
|
||||
expect(decoded).not.toBeNull()
|
||||
expect(decoded!.base).toBe("base")
|
||||
expect(decoded!.style).toBe(DEFAULT_PRESET_CONFIG.style)
|
||||
expect(decoded!.style).toBe("lyra")
|
||||
expect(decoded!.theme).toBe(DEFAULT_PRESET_CONFIG.theme)
|
||||
})
|
||||
|
||||
it("should round-trip all combinations of base and style", () => {
|
||||
for (const base of PRESET_BASES) {
|
||||
for (const style of PRESET_STYLES) {
|
||||
const code = encodePreset({ base, style })
|
||||
const decoded = decodePreset(code)
|
||||
expect(decoded).not.toBeNull()
|
||||
expect(decoded!.base).toBe(base)
|
||||
expect(decoded!.style).toBe(style)
|
||||
}
|
||||
it("should round-trip all styles", () => {
|
||||
for (const style of PRESET_STYLES) {
|
||||
const code = encodePreset({ style })
|
||||
const decoded = decodePreset(code)
|
||||
expect(decoded).not.toBeNull()
|
||||
expect(decoded!.style).toBe(style)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ export const PRESET_RADII = [
|
||||
export const PRESET_MENU_ACCENTS = ["subtle", "bold"] as const
|
||||
export const PRESET_MENU_COLORS = ["default", "inverted"] as const
|
||||
|
||||
// Field definitions in pack order. Total: 43 bits, 10 bits headroom.
|
||||
// Field definitions in pack order. Total: 40 bits, 13 bits headroom.
|
||||
// Note: `base` was removed (was bits 40-42). Old codes are backward-compatible
|
||||
// because `base` was the last field — decoder stops at bit 40 and ignores the rest.
|
||||
const PRESET_FIELDS = [
|
||||
{ key: "menuColor", values: PRESET_MENU_COLORS, bits: 3 },
|
||||
{ key: "menuAccent", values: PRESET_MENU_ACCENTS, bits: 3 },
|
||||
@@ -77,11 +79,9 @@ const PRESET_FIELDS = [
|
||||
{ key: "theme", values: PRESET_THEMES, bits: 6 },
|
||||
{ key: "baseColor", values: PRESET_BASE_COLORS, bits: 6 },
|
||||
{ key: "style", values: PRESET_STYLES, bits: 6 },
|
||||
{ key: "base", values: PRESET_BASES, bits: 3 },
|
||||
] as const
|
||||
|
||||
export type PresetConfig = {
|
||||
base: (typeof PRESET_BASES)[number]
|
||||
style: (typeof PRESET_STYLES)[number]
|
||||
baseColor: (typeof PRESET_BASE_COLORS)[number]
|
||||
theme: (typeof PRESET_THEMES)[number]
|
||||
|
||||
@@ -55,9 +55,6 @@ export async function updateCssVars(
|
||||
overwriteCssVars: options.overwriteCssVars,
|
||||
})
|
||||
await fs.writeFile(cssFilepath, output, "utf8")
|
||||
// Touch the file to ensure dev server file watchers detect the change.
|
||||
const now = new Date()
|
||||
await fs.utimes(cssFilepath, now, now)
|
||||
cssVarsSpinner.succeed()
|
||||
}
|
||||
|
||||
@@ -433,13 +430,13 @@ function updateThemePlugin(cssVars: z.infer<typeof registryItemCssVarsSchema>) {
|
||||
|
||||
if (variable === "radius") {
|
||||
const radiusVariables = {
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) * 0.6)",
|
||||
md: "calc(var(--radius) * 0.8)",
|
||||
lg: "var(--radius)",
|
||||
xl: "calc(var(--radius) + 4px)",
|
||||
"2xl": "calc(var(--radius) + 8px)",
|
||||
"3xl": "calc(var(--radius) + 12px)",
|
||||
"4xl": "calc(var(--radius) + 16px)",
|
||||
xl: "calc(var(--radius) * 1.4)",
|
||||
"2xl": "calc(var(--radius) * 1.8)",
|
||||
"3xl": "calc(var(--radius) * 2.2)",
|
||||
"4xl": "calc(var(--radius) * 2.6)",
|
||||
}
|
||||
for (const [key, value] of Object.entries(radiusVariables)) {
|
||||
const cssVarNode = postcss.decl({
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { registryItemCssSchema } from "@/src/schema"
|
||||
import {
|
||||
registryItemCssSchema,
|
||||
registryItemCssVarsSchema,
|
||||
registryItemTailwindSchema,
|
||||
} from "@/src/schema"
|
||||
import { Config } from "@/src/utils/get-config"
|
||||
import { TailwindVersion } from "@/src/utils/get-project-info"
|
||||
import { highlighter } from "@/src/utils/highlighter"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import { transformCssVars } from "@/src/utils/updaters/update-css-vars"
|
||||
import postcss from "postcss"
|
||||
import AtRule from "postcss/lib/at-rule"
|
||||
import Declaration from "postcss/lib/declaration"
|
||||
@@ -16,13 +22,17 @@ export async function updateCss(
|
||||
config: Config,
|
||||
options: {
|
||||
silent?: boolean
|
||||
cssVars?: z.infer<typeof registryItemCssVarsSchema>
|
||||
cleanupDefaultNextStyles?: boolean
|
||||
overwriteCssVars?: boolean
|
||||
tailwindVersion?: TailwindVersion
|
||||
tailwindConfig?: z.infer<typeof registryItemTailwindSchema>["config"]
|
||||
}
|
||||
) {
|
||||
if (
|
||||
!config.resolvedPaths.tailwindCss ||
|
||||
!css ||
|
||||
Object.keys(css).length === 0
|
||||
) {
|
||||
const hasCss = css && Object.keys(css).length > 0
|
||||
const hasCssVars = Object.keys(options.cssVars ?? {}).length > 0
|
||||
|
||||
if (!config.resolvedPaths.tailwindCss || (!hasCss && !hasCssVars)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,8 +53,23 @@ export async function updateCss(
|
||||
}
|
||||
).start()
|
||||
|
||||
const raw = await fs.readFile(cssFilepath, "utf8")
|
||||
let output = await transformCss(raw, css)
|
||||
let output = await fs.readFile(cssFilepath, "utf8")
|
||||
|
||||
// Apply CSS vars transform first if provided.
|
||||
if (hasCssVars) {
|
||||
output = await transformCssVars(output, options.cssVars!, config, {
|
||||
cleanupDefaultNextStyles: options.cleanupDefaultNextStyles,
|
||||
tailwindVersion: options.tailwindVersion,
|
||||
tailwindConfig: options.tailwindConfig,
|
||||
overwriteCssVars: options.overwriteCssVars,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply CSS transform if provided.
|
||||
if (hasCss) {
|
||||
output = await transformCss(output, css!)
|
||||
}
|
||||
|
||||
await fs.writeFile(cssFilepath, output, "utf8")
|
||||
cssSpinner.succeed()
|
||||
}
|
||||
|
||||
@@ -237,17 +237,11 @@ export async function updateFiles(
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, mergedContent, "utf-8")
|
||||
// Touch the file to ensure dev server file watchers detect the change.
|
||||
const now = new Date()
|
||||
await fs.utimes(filePath, now, now)
|
||||
filesUpdated.push(path.relative(config.resolvedPaths.cwd, filePath))
|
||||
continue
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, content, "utf-8")
|
||||
// Touch the file to ensure dev server file watchers detect the change.
|
||||
const now = new Date()
|
||||
await fs.utimes(filePath, now, now)
|
||||
|
||||
// Handle file creation logging
|
||||
if (!existingFile) {
|
||||
@@ -611,9 +605,6 @@ async function resolveImports(filePaths: string[], config: Config) {
|
||||
|
||||
// Write the updated content to the file.
|
||||
await fs.writeFile(resolvedPath, sourceFile.getFullText(), "utf-8")
|
||||
// Touch the file to ensure dev server file watchers detect the change.
|
||||
const now = new Date()
|
||||
await fs.utimes(resolvedPath, now, now)
|
||||
|
||||
// Track the updated file.
|
||||
updatedFiles.push(filepath)
|
||||
|
||||
@@ -167,6 +167,124 @@ export default function RootLayout({
|
||||
`)
|
||||
})
|
||||
|
||||
it("should use configured utils alias when adding cn import", async () => {
|
||||
const configWithCustomUtilsAlias = {
|
||||
...mockConfig,
|
||||
aliases: {
|
||||
...mockConfig.aliases,
|
||||
utils: "~/lib/utils",
|
||||
},
|
||||
}
|
||||
const input = `
|
||||
import type { Metadata } from "next"
|
||||
import "./globals.css"
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
`
|
||||
const fonts = [
|
||||
{
|
||||
name: "inter",
|
||||
type: "registry:font" as const,
|
||||
font: {
|
||||
family: "Inter",
|
||||
provider: "google" as const,
|
||||
import: "Inter",
|
||||
variable: "--font-sans",
|
||||
subsets: ["latin"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "jetbrains-mono",
|
||||
type: "registry:font" as const,
|
||||
font: {
|
||||
family: "JetBrains Mono",
|
||||
provider: "google" as const,
|
||||
import: "JetBrains_Mono",
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const firstRun = await transformLayoutFonts(
|
||||
input,
|
||||
fonts,
|
||||
configWithCustomUtilsAlias
|
||||
)
|
||||
const secondRun = await transformLayoutFonts(
|
||||
firstRun,
|
||||
fonts,
|
||||
configWithCustomUtilsAlias
|
||||
)
|
||||
|
||||
expect(firstRun).toContain(`import { cn } from "~/lib/utils";`)
|
||||
expect(secondRun).toBe(firstRun)
|
||||
})
|
||||
|
||||
it("should use monorepo utils alias when adding cn import", async () => {
|
||||
const monorepoConfig = {
|
||||
...mockConfig,
|
||||
aliases: {
|
||||
...mockConfig.aliases,
|
||||
utils: "@workspace/ui/lib/utils",
|
||||
},
|
||||
}
|
||||
const input = `
|
||||
import type { Metadata } from "next"
|
||||
import "./globals.css"
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
`
|
||||
const fonts = [
|
||||
{
|
||||
name: "inter",
|
||||
type: "registry:font" as const,
|
||||
font: {
|
||||
family: "Inter",
|
||||
provider: "google" as const,
|
||||
import: "Inter",
|
||||
variable: "--font-sans",
|
||||
subsets: ["latin"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "jetbrains-mono",
|
||||
type: "registry:font" as const,
|
||||
font: {
|
||||
family: "JetBrains Mono",
|
||||
provider: "google" as const,
|
||||
import: "JetBrains_Mono",
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = await transformLayoutFonts(input, fonts, monorepoConfig)
|
||||
|
||||
expect(result).toContain(`import { cn } from "@workspace/ui/lib/utils";`)
|
||||
})
|
||||
|
||||
it("should preserve existing string className", async () => {
|
||||
const input = `
|
||||
import type { Metadata } from "next"
|
||||
|
||||
@@ -162,7 +162,7 @@ async function findLayoutFile(
|
||||
export async function transformLayoutFonts(
|
||||
input: string,
|
||||
fonts: RegistryFontItem[],
|
||||
_config: Config
|
||||
config: Config
|
||||
) {
|
||||
const project = new Project({
|
||||
compilerOptions: {},
|
||||
@@ -254,7 +254,7 @@ export async function transformLayoutFonts(
|
||||
|
||||
// Update html className to include font variables.
|
||||
if (fontVariableNames.length > 0) {
|
||||
updateHtmlClassName(sourceFile, fontVariableNames)
|
||||
updateHtmlClassName(sourceFile, fontVariableNames, config)
|
||||
}
|
||||
|
||||
return sourceFile.getFullText()
|
||||
@@ -335,7 +335,8 @@ function findInsertPosition(
|
||||
|
||||
function updateHtmlClassName(
|
||||
sourceFile: ReturnType<Project["createSourceFile"]>,
|
||||
fontVariableNames: string[]
|
||||
fontVariableNames: string[],
|
||||
config: Config
|
||||
) {
|
||||
// Find the <html> JSX element.
|
||||
const jsxElements = sourceFile.getDescendantsOfKind(
|
||||
@@ -360,7 +361,7 @@ function updateHtmlClassName(
|
||||
})
|
||||
} else {
|
||||
// Need to use cn() for multiple fonts.
|
||||
ensureCnImport(sourceFile)
|
||||
ensureCnImport(sourceFile, config)
|
||||
element.addAttribute({
|
||||
name: "className",
|
||||
initializer: `{cn(${variableExpressions})}`,
|
||||
@@ -387,7 +388,7 @@ function updateHtmlClassName(
|
||||
if (initializer.getKind() === SyntaxKind.StringLiteral) {
|
||||
// className="some-class" -> className={cn("some-class", font.variable)}
|
||||
const currentValue = initializer.getText().slice(1, -1) // Remove quotes.
|
||||
ensureCnImport(sourceFile)
|
||||
ensureCnImport(sourceFile, config)
|
||||
jsxAttr.setInitializer(
|
||||
`{cn("${currentValue}", ${newVarExpressions.join(", ")})}`
|
||||
)
|
||||
@@ -428,19 +429,19 @@ function updateHtmlClassName(
|
||||
if (newVarExpressions.length === 1) {
|
||||
jsxExpr.replaceWithText(`{${newVarExpressions[0]}}`)
|
||||
} else {
|
||||
ensureCnImport(sourceFile)
|
||||
ensureCnImport(sourceFile, config)
|
||||
jsxExpr.replaceWithText(`{cn(${newVarExpressions.join(", ")})}`)
|
||||
}
|
||||
} else if (exprText.startsWith("`") && exprText.endsWith("`")) {
|
||||
// Template literal - parse and convert to cn() arguments.
|
||||
const cnArgs = parseTemplateLiteralToCnArgs(exprText)
|
||||
ensureCnImport(sourceFile)
|
||||
ensureCnImport(sourceFile, config)
|
||||
jsxExpr.replaceWithText(
|
||||
`{cn(${[...cnArgs, ...newVarExpressions].join(", ")})}`
|
||||
)
|
||||
} else {
|
||||
// Some other expression (variable, etc.), wrap with cn().
|
||||
ensureCnImport(sourceFile)
|
||||
ensureCnImport(sourceFile, config)
|
||||
jsxExpr.replaceWithText(
|
||||
`{cn(${exprText}, ${newVarExpressions.join(", ")})}`
|
||||
)
|
||||
@@ -449,7 +450,10 @@ function updateHtmlClassName(
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCnImport(sourceFile: ReturnType<Project["createSourceFile"]>) {
|
||||
function ensureCnImport(
|
||||
sourceFile: ReturnType<Project["createSourceFile"]>,
|
||||
config: Config
|
||||
) {
|
||||
const existingImport = sourceFile.getImportDeclaration((decl) => {
|
||||
const namedImports = decl.getNamedImports()
|
||||
return namedImports.some((imp) => imp.getName() === "cn")
|
||||
@@ -470,7 +474,7 @@ function ensureCnImport(sourceFile: ReturnType<Project["createSourceFile"]>) {
|
||||
} else {
|
||||
// Add a new import for cn.
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: "@/lib/utils",
|
||||
moduleSpecifier: config.aliases.utils,
|
||||
namedImports: ["cn"],
|
||||
})
|
||||
}
|
||||
|
||||
3
packages/shadcn/test/fixtures/frameworks/next-monorepo/app/page.tsx
vendored
Normal file
3
packages/shadcn/test/fixtures/frameworks/next-monorepo/app/page.tsx
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return <div>Hello</div>
|
||||
}
|
||||
6
packages/shadcn/test/fixtures/frameworks/next-monorepo/package.json
vendored
Normal file
6
packages/shadcn/test/fixtures/frameworks/next-monorepo/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "next-monorepo-web",
|
||||
"dependencies": {
|
||||
"next": "15.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/shadcn/test/fixtures/frameworks/next-monorepo/packages/ui/src/globals.css
vendored
Normal file
1
packages/shadcn/test/fixtures/frameworks/next-monorepo/packages/ui/src/globals.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
780
packages/shadcn/test/utils/add-components.test.ts
Normal file
780
packages/shadcn/test/utils/add-components.test.ts
Normal file
@@ -0,0 +1,780 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest"
|
||||
|
||||
import type { Config } from "../../src/utils/get-config"
|
||||
|
||||
// Mock all external dependencies.
|
||||
vi.mock("../../src/registry/resolver", () => ({
|
||||
resolveRegistryTree: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/get-config", async () => {
|
||||
const actual = (await vi.importActual(
|
||||
"../../src/utils/get-config"
|
||||
)) as typeof import("../../src/utils/get-config")
|
||||
return {
|
||||
...actual,
|
||||
getWorkspaceConfig: vi.fn(),
|
||||
findPackageRoot: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("../../src/utils/updaters/update-files", () => ({
|
||||
updateFiles: vi.fn().mockResolvedValue({
|
||||
filesCreated: [],
|
||||
filesUpdated: [],
|
||||
filesSkipped: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/updaters/update-dependencies", () => ({
|
||||
updateDependencies: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/updaters/update-tailwind-config", () => ({
|
||||
updateTailwindConfig: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/updaters/update-env-vars", () => ({
|
||||
updateEnvVars: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/updaters/update-fonts", () => ({
|
||||
updateFonts: vi.fn().mockResolvedValue(undefined),
|
||||
massageTreeForFonts: vi.fn().mockImplementation((tree) => tree),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/updaters/update-css", () => ({
|
||||
updateCss: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/get-project-info", () => ({
|
||||
getProjectTailwindVersionFromConfig: vi.fn().mockResolvedValue("4"),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/spinner", () => ({
|
||||
spinner: vi.fn().mockReturnValue({
|
||||
start: vi.fn().mockReturnThis(),
|
||||
succeed: vi.fn().mockReturnThis(),
|
||||
fail: vi.fn().mockReturnThis(),
|
||||
info: vi.fn().mockReturnThis(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("../../src/utils/logger", () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
import { addComponents } from "../../src/utils/add-components"
|
||||
import { resolveRegistryTree } from "../../src/registry/resolver"
|
||||
import {
|
||||
findPackageRoot,
|
||||
getWorkspaceConfig,
|
||||
} from "../../src/utils/get-config"
|
||||
import { updateFiles } from "../../src/utils/updaters/update-files"
|
||||
import { updateFonts } from "../../src/utils/updaters/update-fonts"
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
function createMockConfig(overrides: Partial<Config> = {}): Config {
|
||||
return {
|
||||
$schema: "https://ui.shadcn.com/schema.json",
|
||||
style: "new-york",
|
||||
rsc: true,
|
||||
tsx: true,
|
||||
tailwind: {
|
||||
css: "app/globals.css",
|
||||
baseColor: "zinc",
|
||||
cssVariables: true,
|
||||
},
|
||||
aliases: {
|
||||
components: "@/components",
|
||||
utils: "@/lib/utils",
|
||||
ui: "@/components/ui",
|
||||
lib: "@/lib",
|
||||
hooks: "@/hooks",
|
||||
},
|
||||
resolvedPaths: {
|
||||
cwd: "/apps/web",
|
||||
tailwindConfig: "/apps/web/tailwind.config.ts",
|
||||
tailwindCss: "/apps/web/app/globals.css",
|
||||
utils: "/apps/web/lib/utils",
|
||||
components: "/apps/web/components",
|
||||
lib: "/apps/web/lib",
|
||||
hooks: "/apps/web/hooks",
|
||||
ui: "/apps/web/components/ui",
|
||||
},
|
||||
...overrides,
|
||||
} as Config
|
||||
}
|
||||
|
||||
describe("addComponents workspace routing", () => {
|
||||
test("should route registry:hook files to workspaceConfig.hooks", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
// Hooks config resolves to the same package but is a distinct config object.
|
||||
// getWorkspaceConfig builds one config per alias key.
|
||||
const hooksConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
hooks: hooksConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "sidebar",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/sidebar.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function Sidebar() {}",
|
||||
},
|
||||
{
|
||||
path: "registry/hooks/use-mobile.ts",
|
||||
type: "registry:hook",
|
||||
content: "export function useMobile() {}",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")
|
||||
|
||||
await addComponents(["sidebar"], appConfig, { silent: true })
|
||||
|
||||
// updateFiles should be called twice: once for registry:ui, once for registry:hook.
|
||||
expect(updateFiles).toHaveBeenCalledTimes(2)
|
||||
|
||||
// First call: registry:ui files with the UI workspace config.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[
|
||||
expect.objectContaining({
|
||||
type: "registry:ui",
|
||||
path: "registry/ui/sidebar.tsx",
|
||||
}),
|
||||
],
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
// Second call: registry:hook files with the hooks workspace config.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[
|
||||
expect.objectContaining({
|
||||
type: "registry:hook",
|
||||
path: "registry/hooks/use-mobile.ts",
|
||||
}),
|
||||
],
|
||||
hooksConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should route registry:lib files to workspaceConfig.lib", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
const libConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
lib: libConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "button",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/button.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function Button() {}",
|
||||
},
|
||||
{
|
||||
path: "registry/lib/utils.ts",
|
||||
type: "registry:lib",
|
||||
content: "export function cn() {}",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")
|
||||
|
||||
await addComponents(["button"], appConfig, { silent: true })
|
||||
|
||||
expect(updateFiles).toHaveBeenCalledTimes(2)
|
||||
|
||||
// First call: registry:ui files with the UI workspace config.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[expect.objectContaining({ type: "registry:ui" })],
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
// Second call: registry:lib files with the lib workspace config.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[expect.objectContaining({ type: "registry:lib" })],
|
||||
libConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should fall back to app config for unmapped types like registry:component", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "login-01",
|
||||
files: [
|
||||
{
|
||||
path: "registry/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
content: "export function LoginForm() {}",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/apps/web")
|
||||
|
||||
await addComponents(["login-01"], appConfig, { silent: true })
|
||||
|
||||
expect(updateFiles).toHaveBeenCalledTimes(1)
|
||||
|
||||
// registry:component should fall back to the app config.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[expect.objectContaining({ type: "registry:component" })],
|
||||
appConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should fall back to app config when workspace key is missing", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
// Workspace config only has ui — no hooks or lib.
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "sidebar",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/sidebar.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function Sidebar() {}",
|
||||
},
|
||||
{
|
||||
path: "registry/hooks/use-mobile.ts",
|
||||
type: "registry:hook",
|
||||
content: "export function useMobile() {}",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")
|
||||
|
||||
await addComponents(["sidebar"], appConfig, { silent: true })
|
||||
|
||||
expect(updateFiles).toHaveBeenCalledTimes(2)
|
||||
|
||||
// registry:ui → workspaceConfig.ui.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[expect.objectContaining({ type: "registry:ui" })],
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
// registry:hook with no workspaceConfig.hooks → falls back to app config.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[expect.objectContaining({ type: "registry:hook" })],
|
||||
appConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should route all three mapped types to their workspace configs", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
hooks: uiConfig,
|
||||
lib: uiConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "sidebar",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/sidebar.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function Sidebar() {}",
|
||||
},
|
||||
{
|
||||
path: "registry/hooks/use-mobile.ts",
|
||||
type: "registry:hook",
|
||||
content: "export function useMobile() {}",
|
||||
},
|
||||
{
|
||||
path: "registry/lib/utils.ts",
|
||||
type: "registry:lib",
|
||||
content: "export function cn() {}",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")
|
||||
|
||||
await addComponents(["sidebar"], appConfig, { silent: true })
|
||||
|
||||
// Three calls: one per type.
|
||||
expect(updateFiles).toHaveBeenCalledTimes(3)
|
||||
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[expect.objectContaining({ type: "registry:ui" })],
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[expect.objectContaining({ type: "registry:hook" })],
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
[expect.objectContaining({ type: "registry:lib" })],
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should fall back to app config for registry:file", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "some-component",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/button.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function Button() {}",
|
||||
},
|
||||
{
|
||||
path: ".env",
|
||||
type: "registry:file",
|
||||
target: "~/.env",
|
||||
content: "API_KEY=xxx",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")
|
||||
|
||||
await addComponents(["some-component"], appConfig, { silent: true })
|
||||
|
||||
expect(updateFiles).toHaveBeenCalledTimes(2)
|
||||
|
||||
// registry:ui → workspace.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[expect.objectContaining({ type: "registry:ui" })],
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
// registry:file → app config.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[expect.objectContaining({ type: "registry:file" })],
|
||||
appConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should default files with no type to registry:ui", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "button",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/button.tsx",
|
||||
// No type — should default to registry:ui.
|
||||
content: "export function Button() {}",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")
|
||||
|
||||
await addComponents(["button"], appConfig, { silent: true })
|
||||
|
||||
expect(updateFiles).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Should route to workspace UI config.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.any(Array),
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should group multiple files of the same type into one updateFiles call", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "sidebar",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/sidebar.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function Sidebar() {}",
|
||||
},
|
||||
{
|
||||
path: "registry/ui/sidebar-nav.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function SidebarNav() {}",
|
||||
},
|
||||
{
|
||||
path: "registry/ui/sidebar-menu.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function SidebarMenu() {}",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")
|
||||
|
||||
await addComponents(["sidebar"], appConfig, { silent: true })
|
||||
|
||||
// All three files are registry:ui — should be one call, not three.
|
||||
expect(updateFiles).toHaveBeenCalledTimes(1)
|
||||
expect(updateFiles).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ path: "registry/ui/sidebar.tsx" }),
|
||||
expect.objectContaining({ path: "registry/ui/sidebar-nav.tsx" }),
|
||||
expect.objectContaining({ path: "registry/ui/sidebar-menu.tsx" }),
|
||||
]),
|
||||
uiConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should route hooks to separate package when aliases differ", async () => {
|
||||
const appConfig = createMockConfig({
|
||||
aliases: {
|
||||
components: "~foo/ui/components",
|
||||
utils: "~foo/ui/lib/utils",
|
||||
hooks: "~foo/hooks/src",
|
||||
lib: "~foo/ui/lib",
|
||||
ui: "~foo/ui/components",
|
||||
},
|
||||
})
|
||||
|
||||
const uiPackageConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
const hooksPackageConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/hooks",
|
||||
tailwindConfig: "/packages/hooks/tailwind.config.ts",
|
||||
tailwindCss: "/packages/hooks/src/globals.css",
|
||||
utils: "/packages/hooks/src/lib/utils",
|
||||
components: "/packages/hooks/src/components",
|
||||
lib: "/packages/hooks/src/lib",
|
||||
hooks: "/packages/hooks/src/hooks",
|
||||
ui: "/packages/hooks/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiPackageConfig,
|
||||
hooks: hooksPackageConfig,
|
||||
lib: uiPackageConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "sidebar",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/sidebar.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function Sidebar() {}",
|
||||
},
|
||||
{
|
||||
path: "registry/hooks/use-mobile.ts",
|
||||
type: "registry:hook",
|
||||
content: "export function useMobile() {}",
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockImplementation(
|
||||
async (_cwd, resolvedPath) => {
|
||||
if (resolvedPath.startsWith("/packages/hooks")) {
|
||||
return "/packages/hooks"
|
||||
}
|
||||
return "/packages/ui"
|
||||
}
|
||||
)
|
||||
|
||||
await addComponents(["sidebar"], appConfig, { silent: true })
|
||||
|
||||
expect(updateFiles).toHaveBeenCalledTimes(2)
|
||||
|
||||
// registry:ui → UI package.
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[expect.objectContaining({ type: "registry:ui" })],
|
||||
uiPackageConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
// registry:hook → hooks package (separate from UI).
|
||||
expect(updateFiles).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[expect.objectContaining({ type: "registry:hook" })],
|
||||
hooksPackageConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test("should call updateFonts with app config, not workspace config", async () => {
|
||||
const appConfig = createMockConfig()
|
||||
const uiConfig = createMockConfig({
|
||||
resolvedPaths: {
|
||||
cwd: "/packages/ui",
|
||||
tailwindConfig: "/packages/ui/tailwind.config.ts",
|
||||
tailwindCss: "/packages/ui/src/globals.css",
|
||||
utils: "/packages/ui/src/lib/utils",
|
||||
components: "/packages/ui/src/components",
|
||||
lib: "/packages/ui/src/lib",
|
||||
hooks: "/packages/ui/src/hooks",
|
||||
ui: "/packages/ui/src/components/ui",
|
||||
},
|
||||
})
|
||||
|
||||
vi.mocked(getWorkspaceConfig).mockResolvedValue({
|
||||
ui: uiConfig,
|
||||
})
|
||||
|
||||
vi.mocked(resolveRegistryTree).mockResolvedValue({
|
||||
name: "button",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/button.tsx",
|
||||
type: "registry:ui",
|
||||
content: "export function Button() {}",
|
||||
},
|
||||
],
|
||||
fonts: [
|
||||
{
|
||||
name: "font-inter",
|
||||
type: "registry:font",
|
||||
title: "Inter",
|
||||
font: {
|
||||
provider: "google",
|
||||
import: "Inter",
|
||||
family: "Inter, sans-serif",
|
||||
variable: "--font-sans",
|
||||
weight: ["400", "500", "600", "700"],
|
||||
subsets: ["latin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
})
|
||||
|
||||
vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")
|
||||
|
||||
await addComponents(["button"], appConfig, { silent: true })
|
||||
|
||||
// updateFonts should use the app config (layout lives in the app).
|
||||
expect(updateFonts).toHaveBeenCalledWith(
|
||||
expect.any(Array),
|
||||
appConfig,
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
// Verify it was NOT called with the workspace UI config.
|
||||
expect(updateFonts).not.toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
uiConfig,
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -44,4 +44,33 @@ describe("get tailwindcss file", async () => {
|
||||
)
|
||||
).toBe(file)
|
||||
})
|
||||
|
||||
test("should use configCssFile when provided and file exists", async () => {
|
||||
const cwd = path.resolve(
|
||||
__dirname,
|
||||
"../fixtures/frameworks/next-monorepo"
|
||||
)
|
||||
expect(
|
||||
await getTailwindCssFile(cwd, "packages/ui/src/globals.css")
|
||||
).toBe("packages/ui/src/globals.css")
|
||||
})
|
||||
|
||||
test("should fall back to glob when configCssFile does not exist", async () => {
|
||||
const cwd = path.resolve(__dirname, "../fixtures/frameworks/next-app")
|
||||
expect(
|
||||
await getTailwindCssFile(cwd, "nonexistent/styles.css")
|
||||
).toBe("app/globals.css")
|
||||
})
|
||||
|
||||
test("should return null when no css file found and no configCssFile", async () => {
|
||||
const cwd = path.resolve(
|
||||
__dirname,
|
||||
"../fixtures/frameworks/next-monorepo"
|
||||
)
|
||||
// The CSS file is nested under packages/ which the glob finds.
|
||||
// Without configCssFile, it should still find it via glob.
|
||||
expect(await getTailwindCssFile(cwd)).toBe(
|
||||
"packages/ui/src/globals.css"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
7
templates/next-app/.prettierignore
Normal file
7
templates/next-app/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.next/
|
||||
.turbo/
|
||||
coverage/
|
||||
pnpm-lock.yaml
|
||||
.pnpm-store/
|
||||
@@ -1,10 +1,13 @@
|
||||
// This configuration only applies to the package manager root.
|
||||
// Root-level ESLint config for a Turborepo workspace.
|
||||
// App/package lint rules live in each workspace's eslint.config.js.
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
ignorePatterns: ["apps/**", "packages/**"],
|
||||
extends: ["@workspace/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
root: true,
|
||||
ignorePatterns: [
|
||||
"**/node_modules/**",
|
||||
"**/.next/**",
|
||||
"**/dist/**",
|
||||
"**/.turbo/**",
|
||||
"**/coverage/**",
|
||||
],
|
||||
}
|
||||
|
||||
7
templates/next-monorepo/.prettierignore
Normal file
7
templates/next-monorepo/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.next/
|
||||
.turbo/
|
||||
coverage/
|
||||
pnpm-lock.yaml
|
||||
.pnpm-store/
|
||||
@@ -27,6 +27,6 @@ export const config = [
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**"],
|
||||
ignores: ["dist/**", ".next/**", "**/.turbo/**", "**/coverage/**"],
|
||||
},
|
||||
]
|
||||
|
||||
7
templates/react-router-app/.prettierignore
Normal file
7
templates/react-router-app/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
.pnpm-store/
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
7
templates/vite-app/.prettierignore
Normal file
7
templates/vite-app/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
.pnpm-store/
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
Reference in New Issue
Block a user