import {Args, Command, Flags} from '@oclif/core' import {loadConfig, validateConfig} from '../helpers/config.js' import {existsSync} from 'node:fs' import {mkdir, writeFile} from 'node:fs/promises' import {join} from 'node:path' import {getAvailableComponentNames, getComponentURL, getRegistry} from '../helpers/registry.js' import ora from 'ora' import React, {ComponentPropsWithoutRef} from 'react' import {render, Box} from 'ink' import {SearchBox} from '../components/SearchBox.js' import {getDirComponentRequiredFiles, checkComponentInstalled} from '../helpers/path.js' import {Choice} from '../components/Choice.js' import {colorize} from '@oclif/core/ux' import {safeFetch} from '../helpers/safeFetcher.js' function Generator() { let complete: boolean = false function ComponentSelector( props: Omit>, 'helper'>, ) { return ( { complete = true props.onSubmit?.(value) }} /> ) } return [ ComponentSelector, new Promise((r) => { const i = setInterval(() => { if (complete) { r() clearInterval(i) } }, 100) }), ] as const } function Generator2() { let complete = false function ForceSelector({onComplete}: {onComplete: (value: 'yes' | 'no') => void}) { return ( { complete = true onComplete(value) }} /> ) } return [ ForceSelector, new Promise((r) => { const i = setInterval(() => { if (complete) { r() clearInterval(i) } }, 100) }), ] as const } export default class Add extends Command { static override args = { name: Args.string({description: 'name of component to install'}), } static override description = 'Add a component to the project.' static override examples = ['<%= config.bin %> <%= command.id %>'] static override flags = { branch: Flags.string({char: 'r', description: 'use other branch instead of main'}), force: Flags.boolean({char: 'f', description: 'override the existing file'}), config: Flags.string({char: 'p', description: 'path to config'}), shared: Flags.string({char: 's', description: 'place for installation of shared.ts'}), components: Flags.string({char: 'c', description: 'place for installation of components'}), } public async run(): Promise { let { args, flags: {force, ...flags}, } = await this.parse(Add) const resolvedConfig = await validateConfig((message: string) => this.log(message), await loadConfig(flags.config)) const componentFolder = join(process.cwd(), resolvedConfig.paths.components) const libFolder = join(process.cwd(), resolvedConfig.paths.lib) if (!existsSync(componentFolder)) { await mkdir(componentFolder, {recursive: true}) } if (!existsSync(libFolder)) { await mkdir(libFolder, {recursive: true}) } const loadRegistryOra = ora('Fetching registry...').start() if (flags.registry) { this.log(`Using ${flags.branch} for branch.`) } const unsafeRegistry = await getRegistry(flags.branch) if (!unsafeRegistry.ok) { loadRegistryOra.fail(unsafeRegistry.message) return } const registry = unsafeRegistry.registry const componentNames = await getAvailableComponentNames(registry) loadRegistryOra.succeed(`Successfully fetched registry! (${componentNames.length} components)`) const searchBoxComponent: {displayName: string; key: string; installed: boolean}[] = [] for await (const name of componentNames) { const installed = await checkComponentInstalled(registry.components[name], resolvedConfig) searchBoxComponent.push({ displayName: installed ? `${name} (installed)` : name, key: name, installed, }) } let name: string | undefined = args.name?.toLowerCase?.() let requireForce: boolean = !name || !componentNames.includes(name.toLowerCase()) ? false : searchBoxComponent.find(({key}) => key === name)?.installed ? !force : false if (!name || !componentNames.includes(name.toLowerCase())) { const [ComponentSelector, waitForComplete] = Generator() const inkInstance = render( { name = comp.key requireForce = comp.installed inkInstance.clear() }} />, ) await waitForComplete inkInstance.unmount() } let quit = false if (requireForce) { const [ForceSelector, waitForComplete] = Generator2() const inkInstance = render( { force = value === 'yes' quit = value === 'no' inkInstance.clear() }} />, ) await waitForComplete inkInstance.unmount() if (quit) { this.log(colorize('redBright', 'Installation canceled by user.')) return } } if (!name || !componentNames.includes(name.toLowerCase())) { this.error('Component name is not provided, or not selected.') } const libFileOra = ora('Installing required library...').start() let successCount = 0 for await (const libFile of registry.lib) { const filePath = join(libFolder, libFile) if (!existsSync(filePath)) { const libFileContentResponse = await safeFetch(registry.base + registry.paths.lib.replace('{libName}', libFile)) if (!libFileContentResponse.ok) { libFileOra.fail(libFileContentResponse.message) return } const libFileContent = await libFileContentResponse.response.text() await writeFile(filePath, libFileContent) successCount++ } } if (successCount > 1) { libFileOra.succeed('Successfully installed library files!') } else { libFileOra.succeed('Library files are already installed!') } const componentFileOra = ora(`Installing ${name} component...`).start() const componentObject = registry.components[name] if (componentObject.type === 'file') { const componentFile = join(componentFolder, registry.components[name].name) if (existsSync(componentFile) && !force) { componentFileOra.succeed(`Component is already installed! (${componentFile})`) } else { const componentFileContentResponse = await safeFetch(await getComponentURL(registry, name)) if (!componentFileContentResponse.ok) { componentFileOra.fail(componentFileContentResponse.message) return } const componentFileContent = (await componentFileContentResponse.response.text()).replaceAll( /import\s+{[^}]*}\s+from\s+"@pswui-lib"/g, (match) => match.replace(/@pswui-lib/, resolvedConfig.import.lib), ) await writeFile(componentFile, componentFileContent) componentFileOra.succeed('Component is successfully installed!') } } else if (componentObject.type === 'dir') { const componentDir = join(componentFolder, componentObject.name) if (!existsSync(componentDir)) { await mkdir(componentDir, {recursive: true}) } const requiredFiles = await getDirComponentRequiredFiles(componentObject, resolvedConfig) if (requiredFiles.length === 0 && !force) { componentFileOra.succeed(`Component is already installed! (${componentDir})`) } else { for await (const filename of requiredFiles) { const componentFile = join(componentDir, filename) if (!existsSync(componentFile) || force) { const componentFileContentResponse = await safeFetch( await getComponentURL(registry, componentObject.name, filename), ) if (!componentFileContentResponse.ok) { componentFileOra.fail(componentFileContentResponse.message) return } const componentFileContent = (await componentFileContentResponse.response.text()).replaceAll( /import\s+{[^}]*}\s+from\s+"@pswui-lib"/g, (match) => match.replace(/@pswui-lib/, resolvedConfig.import.lib), ) await writeFile(componentFile, componentFileContent) } } componentFileOra.succeed('Component is successfully installed!') } } this.log('Now you can import the component.') } }