Skip to content

Commit 27b187f

Browse files
committed
Refactor registry and CLI components for improved functionality
- Added logging to the componentMeta middleware for better debugging. - Changed route handling to use app.get for more specific and less specific routes. - Enhanced the installFromRegistry function to handle proofkitDependencies. - Updated post-install step processing to use structured data for script commands. - Improved error handling in installDependencies to capture and report stderr output. - Adjusted shadcnInstall to provide more informative loading and success messages. - Updated metadata schemas to include proofkitDependencies and structured post-install step data.
1 parent 650e1d5 commit 27b187f

File tree

9 files changed

+222
-94
lines changed

9 files changed

+222
-94
lines changed

apps/docs/src/app/r/[[...name]]/registry.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ const componentMeta = (basePath: string) =>
2626
createMiddleware<{
2727
Variables: { meta: TemplateMetadata; path: string };
2828
}>(async (c, next) => {
29+
console.log("c.req.path", c.req.path);
30+
console.log("basePath", basePath);
2931
const path = c.req.path.replace(basePath, "").replace(/\.json$/, "");
32+
console.log("path", path);
3033
c.set("path", path);
3134

3235
try {
@@ -39,20 +42,28 @@ const componentMeta = (basePath: string) =>
3942
}
4043
});
4144

42-
app.use(componentMeta("/r/meta")).get("/meta/*", async (c) => {
45+
// Handle meta requests first (more specific route)
46+
app.get("/meta/*", componentMeta("/r/meta"), async (c) => {
4347
const meta = c.get("meta");
4448
return c.json(meta, 200);
4549
});
4650

47-
// Handle registry requests at base path "/r"
48-
app.use(componentMeta("/r")).get("/*", async (c) => {
51+
// Handle registry requests at base path "/r" (less specific route)
52+
app.get("/*", componentMeta("/r"), async (c) => {
4953
const path = c.get("path");
54+
const requestUrl = new URL(c.req.url);
5055

5156
const meta = c.get("meta");
5257
if (meta.type === "static") {
5358
try {
5459
const data = await getStaticComponent(path);
55-
return c.json(data);
60+
61+
return c.json({
62+
...data,
63+
registryDependencies: data.registryDependencies?.map((x) =>
64+
x.replace("{proofkit}", requestUrl.origin),
65+
),
66+
});
5667
} catch (error) {
5768
console.error(error);
5869
return c.json({ error: "Component not found." }, { status: 404 });

packages/cli/src/cli/add/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ export const runAdd = async (
2828

2929
if (name === "tanstack-query") {
3030
return await runAddTanstackQueryCommand();
31-
} else if (name === "react-email") {
32-
return await runAddReactEmailCommand({ noInstall: options?.noInstall });
3331
} else if (name !== undefined) {
3432
// an arbitrary name was provided, so we'll try to install from the registry
3533
return await installFromRegistry(name);
@@ -114,6 +112,5 @@ export const makeAddCommand = () => {
114112
addCommand.addCommand(makeAddPageCommand());
115113
addCommand.addCommand(makeAddSchemaCommand());
116114
addCommand.addCommand(makeAddDataSourceCommand());
117-
addCommand.addCommand(makeAddReactEmailCommand());
118115
return addCommand;
119116
};

packages/cli/src/cli/add/registry/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@ export async function installFromRegistry(name: string) {
3434
}
3535

3636
// run shadcn command
37-
await shadcnInstall([url]);
37+
await shadcnInstall([url], meta.title);
3838

3939
// if post-install steps, process those
4040
if (meta.postInstall) {
4141
for (const step of meta.postInstall) {
4242
await processPostInstallStep(step);
4343
}
4444
}
45+
46+
if (meta.proofkitDependencies) {
47+
for (const dependency of meta.proofkitDependencies) {
48+
await installFromRegistry(dependency);
49+
}
50+
}
4551
}

packages/cli/src/cli/add/registry/postInstall/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export async function processPostInstallStep(step: PostInstallStep) {
1010
state.projectDir; // current working directory
1111
const packageJsonPath = path.join(state.projectDir, "package.json");
1212
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
13-
packageJson.scripts[step.name] = step.script;
13+
packageJson.scripts[step.data.scriptName] = step.data.scriptCommand;
1414
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
1515
} else if (step.action === "wrap provider") {
1616
// TODO: implement

packages/cli/src/helpers/installDependencies.ts

Lines changed: 106 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,51 @@ const execWithSpinner = async (
2828
const spinner = ora(
2929
options.loadingMessage ?? `Running ${pkgManager} ${args.join(" ")} ...`
3030
).start();
31-
const subprocess = execa(pkgManager, args, { cwd: projectDir, stdout });
31+
const subprocess = execa(pkgManager, args, {
32+
cwd: projectDir,
33+
stdout,
34+
stderr: "pipe", // Capture stderr to get error messages
35+
});
3236

3337
await new Promise<void>((res, rej) => {
38+
let stdoutOutput = "";
39+
let stderrOutput = "";
40+
3441
if (onDataHandle) {
3542
subprocess.stdout?.on("data", onDataHandle(spinner));
43+
} else {
44+
// If no custom handler, capture stdout for error reporting
45+
subprocess.stdout?.on("data", (data) => {
46+
stdoutOutput += data.toString();
47+
});
3648
}
3749

50+
// Capture stderr output for error reporting
51+
subprocess.stderr?.on("data", (data) => {
52+
stderrOutput += data.toString();
53+
});
54+
3855
void subprocess.on("error", (e) => rej(e));
39-
void subprocess.on("close", () => res());
56+
void subprocess.on("close", (code) => {
57+
if (code === 0) {
58+
res();
59+
} else {
60+
// Combine stdout and stderr for complete error message
61+
const combinedOutput = [stdoutOutput, stderrOutput]
62+
.filter((output) => output.trim())
63+
.join("\n")
64+
.trim()
65+
// Remove spinner-related lines that aren't useful in error output
66+
.replace(/^- Checking registry\.$/gm, "")
67+
.replace(/^\s*$/gm, "") // Remove empty lines
68+
.trim();
69+
70+
const errorMessage =
71+
combinedOutput ||
72+
`Command failed with exit code ${code}: ${pkgManager} ${args.join(" ")}`;
73+
rej(new Error(errorMessage));
74+
}
75+
});
4076
});
4177

4278
return spinner;
@@ -98,28 +134,41 @@ export const runExecCommand = async ({
98134
command,
99135
projectDir = state.projectDir,
100136
successMessage,
137+
errorMessage,
101138
loadingMessage,
102139
}: {
103140
command: string[];
104141
projectDir?: string;
105142
successMessage?: string;
143+
errorMessage?: string;
106144
loadingMessage?: string;
107145
}) => {
108-
const spinner = await _runExecCommand({
109-
projectDir,
110-
command,
111-
loadingMessage,
112-
});
146+
let spinner: Ora | null = null;
113147

114-
// If the spinner was used to show the progress, use succeed method on it
115-
// If not, use the succeed on a new spinner
116-
(spinner ?? ora()).succeed(
117-
chalk.green(
118-
successMessage
119-
? `${successMessage}\n`
120-
: `Successfully ran ${command.join(" ")}!\n`
121-
)
122-
);
148+
try {
149+
spinner = await _runExecCommand({
150+
projectDir,
151+
command,
152+
loadingMessage,
153+
});
154+
155+
// If the spinner was used to show the progress, use succeed method on it
156+
// If not, use the succeed on a new spinner
157+
(spinner ?? ora()).succeed(
158+
chalk.green(
159+
successMessage
160+
? `${successMessage}\n`
161+
: `Successfully ran ${command.join(" ")}!\n`
162+
)
163+
);
164+
} catch (error) {
165+
// If we have a spinner, fail it, otherwise just throw the error
166+
if (spinner) {
167+
const failMessage = errorMessage || `Failed to run ${command.join(" ")}`;
168+
spinner.fail(chalk.red(failMessage));
169+
}
170+
throw error;
171+
}
123172
};
124173

125174
export const _runExecCommand = async ({
@@ -134,36 +183,63 @@ export const _runExecCommand = async ({
134183
}): Promise<Ora | null> => {
135184
const pkgManager = getUserPkgManager();
136185
switch (pkgManager) {
137-
// When using npm, inherit the stderr stream so that the progress bar is shown
186+
// When using npm, capture both stdout and stderr to show error messages
138187
case "npm":
139-
await execa("npx", [...command], {
188+
const result = await execa("npx", [...command], {
140189
cwd: projectDir,
141-
stderr: "inherit",
190+
stdout: "pipe",
191+
stderr: "pipe",
192+
reject: false,
142193
});
143194

195+
if (result.exitCode !== 0) {
196+
// Combine stdout and stderr for complete error message
197+
const combinedOutput = [result.stdout, result.stderr]
198+
.filter((output) => output?.trim())
199+
.join("\n")
200+
.trim()
201+
// Remove spinner-related lines that aren't useful in error output
202+
.replace(/^- Checking registry\.$/gm, "")
203+
.replace(/^\s*$/gm, "") // Remove empty lines
204+
.trim();
205+
206+
const errorMessage =
207+
combinedOutput ||
208+
`Command failed with exit code ${result.exitCode}: npx ${command.join(" ")}`;
209+
throw new Error(errorMessage);
210+
}
211+
144212
return null;
145213
// When using yarn or pnpm, use the stdout stream and ora spinner to show the progress
146214
case "pnpm":
215+
// For shadcn commands, don't use progress handler to capture full output
216+
const isInstallCommand = command.includes("install");
147217
return execWithSpinner(projectDir, "pnpm", {
148218
args: ["dlx", ...command],
149219
loadingMessage,
150-
onDataHandle: (spinner) => (data) => {
151-
const text = data.toString();
220+
onDataHandle: isInstallCommand
221+
? (spinner) => (data) => {
222+
const text = data.toString();
152223

153-
if (text.includes("Progress")) {
154-
spinner.text = text.includes("|")
155-
? (text.split(" | ")[1] ?? "")
156-
: text;
157-
}
158-
},
224+
if (text.includes("Progress")) {
225+
spinner.text = text.includes("|")
226+
? (text.split(" | ")[1] ?? "")
227+
: text;
228+
}
229+
}
230+
: undefined,
159231
});
160232
case "yarn":
233+
// For shadcn commands, don't use progress handler to capture full output
234+
const isYarnInstallCommand = command.includes("install");
161235
return execWithSpinner(projectDir, pkgManager, {
162236
args: [...command],
163237
loadingMessage,
164-
onDataHandle: (spinner) => (data) => {
165-
spinner.text = data.toString();
166-
},
238+
onDataHandle: isYarnInstallCommand
239+
? (spinner) => (data) => {
240+
spinner.text = data.toString();
241+
}
242+
: undefined,
167243
});
168244
// When using bun, the stdout stream is ignored and the spinner is shown
169245
case "bun":

packages/cli/src/helpers/shadcn-cli.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { DEFAULT_REGISTRY_URL } from "~/consts.js";
2+
import { logger } from "~/utils/logger.js";
23
import { getSettings } from "~/utils/parseSettings.js";
34
import { runExecCommand } from "./installDependencies.js";
45

5-
export async function shadcnInstall(components: string | string[]) {
6+
export async function shadcnInstall(
7+
components: string | string[],
8+
friendlyComponentName?: string
9+
) {
610
const componentsArray = Array.isArray(components) ? components : [components];
7-
const command = ["shadcn@latest", "add", ...componentsArray];
11+
const command = ["shadcn@latest", "add", "--overwrite", ...componentsArray];
812
await runExecCommand({
913
command,
10-
loadingMessage: "Installing components...",
11-
successMessage: "Components installed successfully!",
14+
loadingMessage: `Installing ${friendlyComponentName ?? "components"}...`,
15+
successMessage: `${friendlyComponentName ?? "Components"} installed successfully!`,
16+
errorMessage: `Failed to install ${friendlyComponentName ?? "components"}`,
1217
});
1318
}
1419

0 commit comments

Comments
 (0)