@react-three/uikit
Version:
Build performant 3D user interfaces with react-three-fiber and yoga.
149 lines (148 loc) • 5.99 kB
JavaScript
import { Command } from 'commander';
import { ZodError, z } from 'zod';
import prompts from 'prompts';
import chalk from 'chalk';
import { resolve } from 'path';
import { createWriteStream, existsSync } from 'fs';
import ora from 'ora';
import { Readable } from 'stream';
import { finished } from 'stream/promises';
import { cwd } from 'process';
import { mkdir } from 'fs/promises';
const commandOptionsSchema = z.object({
path: z.string().optional(),
overwrite: z.boolean(),
cwd: z.string(),
});
const kitAndComponentsSchema = z.array(z.string()).min(2);
const BASE_URL = 'https://raw.githubusercontent.com/pmndrs/uikit/main/packages/kits/';
export const add = new Command('add')
.description('add a component to your project')
.argument('<kit> <components...>', 'components from kit to add')
.option('-p, --path <path>', 'the path to add the component to.')
.option('-o, --overwrite', 'overwrite existing files.', false)
.option('-c, --cwd <cwd>', 'the working directory. defaults to the current directory.', cwd())
.action(async (c, opts) => {
try {
let [kit, ...components] = kitAndComponentsSchema
.parse(c, {
errorMap: (issue) => {
if (issue.code === 'too_small') {
return {
message: 'Command requires <kit> and <components...>',
};
}
return { message: issue.message ?? '' };
},
})
.map((s) => s.toLowerCase());
let { overwrite, path, cwd } = commandOptionsSchema.parse(opts);
let registry;
try {
registry = await getRegistry(kit);
}
catch (e) {
throw `Unable to fetch registry for ${kit} kit: ${getErrorString(e)}`;
}
if (path == null) {
path = (await prompts({
type: 'text',
name: 'path',
message: `Configure the path for the ${chalk.cyan('components')}:`,
initial: 'src/components',
})).path;
}
const absPath = resolve(cwd, path, kit);
if (!existsSync(absPath)) {
await mkdir(absPath, { recursive: true });
}
const absThemePath = resolve(absPath, 'theme.tsx');
if (!existsSync(absThemePath)) {
await download(kit, 'base-theme.tsx', absThemePath);
}
const componentsToInstall = new Set();
for (const component of components) {
componentsToInstall.add(component);
const registryEntry = registry[component];
if (registryEntry == null) {
throw `component ${kit} ${component} is not in the registry`;
}
if (registryEntry.registryDependencies == null) {
continue;
}
for (const dependency of registryEntry.registryDependencies) {
componentsToInstall.add(dependency);
}
}
const spinner = ora(`Installing ${kit} components...`).start();
component: for (const component of componentsToInstall) {
try {
spinner.text = `Installing ${kit} ${component}...`;
const registryEntry = registry[component];
if (registryEntry == null) {
throw `component not in registry`;
}
const files = registryEntry.files;
if (!overwrite) {
for (const file of files) {
const absFilePath = resolve(absPath, file);
if (existsSync(absFilePath)) {
spinner.stop();
const { overwrite } = await prompts({
type: 'confirm',
name: 'overwrite',
message: `Component ${component} already exists. Would you like to overwrite?`,
initial: false,
});
if (overwrite === false) {
console.log(chalk.cyan(`Installing ${kit} ${component} was skipped to prevent overwriting ${file}.`));
continue component;
}
spinner.start(`Installing ${kit} ${component}...`);
}
}
}
for (const file of files) {
const absFilePath = resolve(absPath, file);
download(kit, file, absFilePath);
}
}
catch (e) {
throw `Unable to install ${kit} ${component}: ${getErrorString(e)}`;
}
}
spinner.succeed(`Done.`);
}
catch (e) {
console.log(chalk.red(getErrorString(e)));
process.exit(1);
}
});
async function download(kit, file, targetAbsolutePath) {
const response = await fetch(`${BASE_URL}${kit}/src/${file}`);
if (response.body == null) {
throw new Error(`Invalid response when downloading ${file} from registry: ${response.statusText}`);
}
const stream = createWriteStream(targetAbsolutePath);
await finished(Readable.fromWeb(response.body).pipe(stream));
}
function getErrorString(error) {
if (typeof error === 'string') {
return error;
}
else if (error instanceof ZodError) {
return error.format()._errors.join('\n');
}
else if (error instanceof Error) {
return error.message;
}
return 'Something went wrong. Please try again.';
}
const registrySchema = z.record(z.string(), z.object({
files: z.array(z.string()),
registryDependencies: z.array(z.string()).optional(),
}));
async function getRegistry(kit) {
const data = await (await fetch(`${BASE_URL}${kit}/src/registry.json`)).json();
return registrySchema.parse(data);
}