Skip to content

Commit

Permalink
feat: Auto-detect config files (#971)
Browse files Browse the repository at this point in the history
  • Loading branch information
AdrianGonz97 authored Mar 29, 2024
1 parent ebd04c4 commit c5afe89
Show file tree
Hide file tree
Showing 15 changed files with 171 additions and 76 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-clouds-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn-svelte": patch
---

chore: Improved printed error messages
5 changes: 5 additions & 0 deletions .changeset/rude-seahorses-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn-svelte": patch
---

chore: Certain config files can now be auto-detected and suggested during `init`
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"radix-svelte"
],
"scripts": {
"dev": "tsup --watch",
"dev": "tsup --watch --sourcemap",
"build": "tsup --minify",
"check": "tsc --noEmit",
"start:dev": "cross-env COMPONENTS_REGISTRY_URL=http://localhost:5173 node dist/index.js",
Expand All @@ -54,6 +54,7 @@
"@types/node": "^18.19.22",
"cross-env": "^7.0.3",
"get-tsconfig": "^4.7.3",
"ignore": "^5.3.1",
"prettier": "^3.0.0",
"sisteransi": "^1.0.5",
"tsup": "^8.0.0",
Expand Down
17 changes: 6 additions & 11 deletions packages/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as v from "valibot";
import { getConfig, type Config } from "../utils/get-config.js";
import { getEnvProxy } from "../utils/get-env-proxy.js";
import { getPackageManager } from "../utils/get-package-manager.js";
import { error, handleError } from "../utils/handle-error.js";
import { ConfigError, error, handleError } from "../utils/errors.js";
import {
fetchTree,
getItemTargetPath,
Expand Down Expand Up @@ -42,7 +42,7 @@ export const add = new Command()
.option("-a, --all", "install all components to your project.", false)
.option("-y, --yes", "skip confirmation prompt.", false)
.option("-o, --overwrite", "overwrite existing files.", false)
.option("--proxy <proxy>", "fetch components from registry using a proxy.")
.option("--proxy <proxy>", "fetch components from registry using a proxy.", getEnvProxy())
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
Expand All @@ -65,7 +65,7 @@ export const add = new Command()

const config = await getConfig(cwd);
if (!config) {
throw error(
throw new ConfigError(
`Configuration file is missing. Please run ${color.green("init")} to create a ${highlight("components.json")} file.`
);
}
Expand All @@ -79,14 +79,9 @@ export const add = new Command()
});

async function runAdd(cwd: string, config: Config, options: AddOptions) {
const proxy = options.proxy ?? getEnvProxy();
if (proxy) {
const isCustom = !!options.proxy;
if (isCustom) process.env.HTTP_PROXY = options.proxy;

p.log.warn(
`You are using a ${isCustom ? "provided" : "system environment"} proxy: ${color.green(proxy)}`
);
if (options.proxy !== undefined) {
process.env.HTTP_PROXY = options.proxy;
p.log.info(`You are using the provided proxy: ${color.green(options.proxy)}`);
}

const registryIndex = await getRegistryIndex();
Expand Down
42 changes: 26 additions & 16 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { execa } from "execa";
import * as cliConfig from "../utils/get-config.js";
import type { Config } from "../utils/get-config.js";
import { getPackageManager } from "../utils/get-package-manager.js";
import { error, handleError } from "../utils/handle-error.js";
import { error, handleError } from "../utils/errors.js";
import { getRegistryBaseColor, getRegistryBaseColors, getRegistryStyles } from "../utils/registry";
import * as templates from "../utils/templates.js";
import * as p from "../utils/prompts.js";
import { intro } from "../utils/prompt-helpers.js";
import { resolveImport } from "../utils/resolve-imports.js";
import { syncSvelteKit } from "../utils/sync-sveltekit.js";
import { syncSvelteKit } from "../utils/sveltekit.js";
import { detectConfigs } from "../utils/auto-detect.js";

const PROJECT_DEPENDENCIES = ["tailwind-variants", "clsx", "tailwind-merge"] as const;
const highlight = (...args: unknown[]) => color.bold.cyan(...args);
Expand Down Expand Up @@ -48,12 +49,13 @@ export const init = new Command()
}
});

async function promptForConfig(cwd: string, defaultConfig: Config | null = null) {
async function promptForConfig(cwd: string, defaultConfig: Config | null) {
// if it's a SvelteKit project, run sync so that the aliases are always up to date
await syncSvelteKit(cwd);

const styles = await getRegistryStyles();
const baseColors = await getRegistryBaseColors();
const detectedConfigs = detectConfigs(cwd, { relative: true });

const typescript = await p.confirm({
message: `Would you like to use ${highlight("TypeScript")} (recommended)?`,
Expand Down Expand Up @@ -100,10 +102,13 @@ async function promptForConfig(cwd: string, defaultConfig: Config | null = null)
tailwindCss: () =>
p.text({
message: `Where is your ${highlight("global CSS")} file?`,
initialValue: defaultConfig?.tailwind.css ?? cliConfig.DEFAULT_TAILWIND_CSS,
placeholder: cliConfig.DEFAULT_TAILWIND_CSS,
initialValue:
defaultConfig?.tailwind.css ??
detectedConfigs.cssPath ??
cliConfig.DEFAULT_TAILWIND_CSS,
placeholder: detectedConfigs.cssPath ?? cliConfig.DEFAULT_TAILWIND_CSS,
validate: (value) => {
if (existsSync(path.resolve(cwd, value))) {
if (value && existsSync(path.resolve(cwd, value))) {
return;
}
return `"${color.bold(value)}" does not exist. Please enter a valid path.`;
Expand All @@ -112,10 +117,13 @@ async function promptForConfig(cwd: string, defaultConfig: Config | null = null)
tailwindConfig: () =>
p.text({
message: `Where is your ${highlight("Tailwind config")} located?`,
initialValue: defaultConfig?.tailwind.config ?? cliConfig.DEFAULT_TAILWIND_CONFIG,
placeholder: cliConfig.DEFAULT_TAILWIND_CONFIG,
initialValue:
defaultConfig?.tailwind.config ??
detectedConfigs.tailwindPath ??
cliConfig.DEFAULT_TAILWIND_CONFIG,
placeholder: detectedConfigs.tailwindPath ?? cliConfig.DEFAULT_TAILWIND_CONFIG,
validate: (value) => {
if (existsSync(path.resolve(cwd, value))) {
if (value && existsSync(path.resolve(cwd, value))) {
return;
}
return `"${color.bold(value)}" does not exist. Please enter a valid path.`;
Expand All @@ -124,14 +132,18 @@ async function promptForConfig(cwd: string, defaultConfig: Config | null = null)
components: () =>
p.text({
message: `Configure the import alias for ${highlight("components")}:`,
initialValue: defaultConfig?.aliases["components"] ?? cliConfig.DEFAULT_COMPONENTS,
initialValue: defaultConfig?.aliases.components ?? cliConfig.DEFAULT_COMPONENTS,
placeholder: cliConfig.DEFAULT_COMPONENTS,
validate: validateImportAlias,
}),
utils: () =>
utils: ({ results: { components } }) =>
p.text({
message: `Configure the import alias for ${highlight("utils")}:`,
initialValue: defaultConfig?.aliases["utils"] ?? cliConfig.DEFAULT_UTILS,
initialValue:
defaultConfig?.aliases.utils ??
// infers the alias from `components`. if `components = @/comps` then suggest `utils = @/utils`
components?.split("/").slice(0, -1).join("/") + "/utils" ??
cliConfig.DEFAULT_UTILS,
placeholder: cliConfig.DEFAULT_UTILS,
validate: validateImportAlias,
}),
Expand Down Expand Up @@ -159,8 +171,10 @@ async function promptForConfig(cwd: string, defaultConfig: Config | null = null)
},
});

// Delete `tailwind.config.cjs` and rename to `.js`
if (config.tailwind.config.endsWith(".cjs")) {
p.log.info(`Your tailwind config has been renamed to ${highlight("tailwind.config.js")}.`);
await fs.unlink(config.tailwind.config).catch(() => null);
const renamedTailwindConfigPath = config.tailwind.config.replace(".cjs", ".js");
config.tailwind.config = renamedTailwindConfigPath;
}
Expand Down Expand Up @@ -209,10 +223,6 @@ export async function runInit(cwd: string, config: Config) {
"utf8"
);

// Delete tailwind.config.cjs, if present
const cjsConfig = config.resolvedPaths.tailwindConfig.replace(".js", ".cjs");
if (cjsConfig.endsWith(".cjs")) await fs.unlink(cjsConfig).catch((e) => e); // throws when it DNE

// Write css file.
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor);
if (baseColor) {
Expand Down
18 changes: 6 additions & 12 deletions packages/cli/src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { execa } from "execa";
import * as v from "valibot";
import { getConfig, type Config } from "../utils/get-config.js";
import { getPackageManager } from "../utils/get-package-manager.js";
import { handleError, error } from "../utils/handle-error.js";
import { handleError, error } from "../utils/errors.js";
import { fetchTree, getItemTargetPath, getRegistryIndex, resolveTree } from "../utils/registry";
import { UTILS } from "../utils/templates.js";
import { transformImports } from "../utils/transformers.js";
Expand All @@ -32,7 +32,7 @@ export const update = new Command()
.argument("[components...]", "name of components")
.option("-a, --all", "update all existing components.", false)
.option("-y, --yes", "skip confirmation prompt.", false)
.option("--proxy <proxy>", "fetch components from registry using a proxy.")
.option("--proxy <proxy>", "fetch components from registry using a proxy.", getEnvProxy())
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
Expand Down Expand Up @@ -73,18 +73,12 @@ export const update = new Command()
});

async function runUpdate(cwd: string, config: Config, options: UpdateOptions) {
const components = options.components;

const proxy = options.proxy ?? getEnvProxy();
if (proxy) {
const isCustom = !!options.proxy;
if (isCustom) process.env.HTTP_PROXY = options.proxy;

p.log.warn(
`You are using a ${isCustom ? "provided" : "system environment"} proxy: ${color.green(proxy)}`
);
if (options.proxy !== undefined) {
process.env.HTTP_PROXY = options.proxy;
p.log.info(`You are using the provided proxy: ${color.green(options.proxy)}`);
}

const components = options.components;
const registryIndex = await getRegistryIndex();

const componentDir = path.resolve(config.resolvedPaths.components, "ui");
Expand Down
68 changes: 68 additions & 0 deletions packages/cli/src/utils/auto-detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import fs from "node:fs";
import path from "node:path";
import ignore, { type Ignore } from "ignore";

const STYLESHEETS = ["app.css", "app.pcss", "app.postcss", "main.css", "main.pcss", "main.postcss"];
const TAILWIND_CONFIGS = [
"tailwind.config.js",
"tailwind.config.cjs",
"tailwind.config.mjs",
"tailwind.config.ts",
];

// commonly ignored
const IGNORE = ["node_modules", ".git", ".svelte-kit"];

export function detectConfigs(cwd: string, config?: { relative: boolean }) {
let tailwindPath, cssPath;
const paths = findFiles(cwd);
for (const filepath of paths) {
const filename = path.parse(filepath).base;
if (cssPath === undefined && STYLESHEETS.includes(filename)) {
cssPath = config?.relative ? path.relative(cwd, filepath) : filepath;
}
if (tailwindPath === undefined && TAILWIND_CONFIGS.includes(filename)) {
tailwindPath = config?.relative ? path.relative(cwd, filepath) : filepath;
}
}
return { tailwindPath, cssPath };
}

/**
* Walks down the directory tree, returning file paths that are _not_ ignored from their respective `.gitignore`'s
*/
function findFiles(dirPath: string) {
return find(dirPath, []);
}

function find(dirPath: string, ignores: { dirPath: string; ig: Ignore }[]): string[] {
const paths: string[] = [];
const files = fs.readdirSync(dirPath, { withFileTypes: true });
const ignorePath = path.join(dirPath, ".gitignore");
if (fs.existsSync(ignorePath)) {
const gitignore = fs.readFileSync(ignorePath, { encoding: "utf8" });
const ig = ignore().add(gitignore);
ignores.push({ dirPath, ig });
}

for (const file of files) {
const filepath = path.join(file.path, file.name);
// ignore any of the common suspects
if (IGNORE.some((name) => file.path.includes(name))) continue;

// check if file is ignored
const ignored = ignores.some((parent) => {
// make the path relative to the parent
const relative = path.relative(parent.dirPath, filepath);
if (ignore.isPathValid(relative) === false) return;

return parent.ig.ignores(relative);
});
if (ignored) continue;

if (file.isFile()) paths.push(filepath);
if (file.isDirectory()) paths.push(...find(filepath, ignores));
}

return paths;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ export function handleError(error: unknown) {
process.exit(1);
}

if (error instanceof CLIError || error instanceof ConfigError) {
p.cancel(`[${error.name}]: ${error.message}`);
process.exit(1);
}

// unexpected error
if (error instanceof Error) {
p.cancel(error.message);
p.cancel(error.stack);
process.exit(1);
}

Expand All @@ -19,5 +25,13 @@ export function handleError(error: unknown) {
}

export function error(msg: string) {
return new Error(msg);
return new CLIError(msg);
}

export class CLIError extends Error {
name = "CLI Error";
}

export class ConfigError extends Error {
name = "Config Error";
}
18 changes: 10 additions & 8 deletions packages/cli/src/utils/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import fs from "node:fs";
import path from "node:path";
import color from "chalk";
import * as v from "valibot";
import { ConfigError, error } from "./errors.js";
import { getTsconfig } from "get-tsconfig";
import { resolveImport } from "./resolve-imports.js";
import { syncSvelteKit } from "./sync-sveltekit.js";
import { syncSvelteKit } from "./sveltekit.js";

export const DEFAULT_STYLE = "default";
export const DEFAULT_COMPONENTS = "$lib/components";
Expand Down Expand Up @@ -66,17 +67,18 @@ export async function resolveConfigPaths(cwd: string, config: RawConfig) {
const pathAliases = getTSConfig(cwd, tsconfigType);

if (pathAliases === null) {
throw new Error(
throw error(
`Missing ${highlight("paths")} field in your ${highlight(tsconfigType)} for path aliases. See: ${color.underline("https://www.shadcn-svelte.com/docs/installation/manual#configure-path-aliases")}`
);
}

const utilsPath = resolveImport(config.aliases.utils, pathAliases);
const componentsPath = resolveImport(config.aliases.components, pathAliases);

const aliasError = (type: string, alias: string) =>
new Error(
`[components.json]: Invalid ${highlight(type)} import alias: ${highlight(alias)}. Import aliases ${color.underline("must use")} existing path aliases defined in your ${highlight(tsconfigType)}. See: ${color.underline("https://www.shadcn-svelte.com/docs/installation/manual#configure-path-aliases")}.`
new ConfigError(
`Invalid import alias found: (${highlight(`"${type}": "${alias}"`)}) in ${highlight("components.json")}.
- Import aliases ${color.underline("must use")} existing path aliases defined in your ${highlight(tsconfigType)} (e.g. "${type}": "$lib/${type}").
- See: ${color.underline("https://www.shadcn-svelte.com/docs/installation/manual#configure-path-aliases")}.`
);

if (utilsPath === undefined) throw aliasError("utils", config.aliases.utils);
Expand All @@ -96,7 +98,7 @@ export async function resolveConfigPaths(cwd: string, config: RawConfig) {
export function getTSConfig(cwd: string, tsconfigName: "tsconfig.json" | "jsconfig.json") {
const parsedConfig = getTsconfig(path.resolve(cwd, "package.json"), tsconfigName);
if (parsedConfig === null) {
throw new Error(
throw error(
`Failed to find ${highlight(tsconfigName)}. See: ${color.underline("https://www.shadcn-svelte.com/docs/installation#opt-out-of-typescript")}`
);
}
Expand All @@ -121,7 +123,7 @@ export async function getRawConfig(cwd: string): Promise<RawConfig | null> {
const config = JSON.parse(configResult);

return v.parse(rawConfigSchema, config);
} catch (error) {
throw new Error(`[components.json]: Invalid configuration found in ${highlight(configPath)}.`);
} catch (err) {
throw new ConfigError(`Invalid configuration found in ${highlight(configPath)}.`);
}
}
Loading

0 comments on commit c5afe89

Please sign in to comment.