From 5fb17c663d768043423c866366f5a2a47fba6900 Mon Sep 17 00:00:00 2001 From: Arnav K Date: Thu, 13 Feb 2025 22:24:57 +0530 Subject: [PATCH] refactor: init & clone (#63) --- cli/clone/register.ts | 203 ++++++++----------------- cli/init/register.ts | 152 ++++++------------ lib/shared/detect-pkg-manager.ts | 15 ++ lib/shared/generate-ts-config.ts | 34 +++++ lib/shared/setup-tsci-packages.ts | 68 +++++++++ lib/shared/write-file-if-not-exists.ts | 10 ++ 6 files changed, 235 insertions(+), 247 deletions(-) create mode 100644 lib/shared/detect-pkg-manager.ts create mode 100644 lib/shared/generate-ts-config.ts create mode 100644 lib/shared/setup-tsci-packages.ts create mode 100644 lib/shared/write-file-if-not-exists.ts diff --git a/cli/clone/register.ts b/cli/clone/register.ts index 5838aaf..5026808 100644 --- a/cli/clone/register.ts +++ b/cli/clone/register.ts @@ -2,53 +2,8 @@ import type { Command } from "commander" import { getKy } from "lib/registry-api/get-ky" import * as fs from "node:fs" import * as path from "node:path" -import { execSync } from "node:child_process" - -// Detect the package manager being used in the project -const detectPackageManager = (): string => { - const userAgent = process.env.npm_config_user_agent || "" - if (userAgent.startsWith("yarn")) return "yarn" - if (userAgent.startsWith("pnpm")) return "pnpm" - if (userAgent.startsWith("bun")) return "bun" - - if (fs.existsSync("yarn.lock")) return "yarn" - if (fs.existsSync("pnpm-lock.yaml")) return "pnpm" - if (fs.existsSync("bun.lockb")) return "bun" - - return "npm" // Default to npm -} - -// Generate a React-compatible tsconfig.json -const generateTsConfig = (dir: string) => { - const tsconfigPath = path.join(dir, "tsconfig.json") - const tsconfigContent = JSON.stringify( - { - compilerOptions: { - target: "ES6", - module: "ESNext", - jsx: "react-jsx", - outDir: "dist", - strict: true, - esModuleInterop: true, - moduleResolution: "node", - skipLibCheck: true, - forceConsistentCasingInFileNames: true, - resolveJsonModule: true, - sourceMap: true, - allowSyntheticDefaultImports: true, - experimentalDecorators: true, - }, - }, - null, - 2, - ) - if (!fs.existsSync(tsconfigPath)) { - fs.writeFileSync(tsconfigPath, tsconfigContent.trimStart()) - console.log(`Created: ${tsconfigPath}`) - } else { - console.log(`Skipped: ${tsconfigPath} already exists`) - } -} +import { setupTsciProject } from "lib/shared/setup-tsci-packages" +import { generateTsConfig } from "lib/shared/generate-ts-config" export const registerClone = (program: Command) => { program @@ -56,120 +11,92 @@ export const registerClone = (program: Command) => { .description("Clone a snippet from the registry") .argument("", "Snippet to clone (e.g. author/snippetName)") .action(async (snippetPath: string) => { - let author: string - let snippetName: string - if (!snippetPath.startsWith("@tsci/") && snippetPath.includes("/")) { - ;[author, snippetName] = snippetPath.split("/") - } else { - const trimmedPath = snippetPath.replace("@tsci/", "") - const firstDotIndex = trimmedPath.indexOf(".") - author = trimmedPath.slice(0, firstDotIndex) - snippetName = trimmedPath.slice(firstDotIndex + 1) - } - - if (!author || !snippetName) { + const match = snippetPath.match(/^(?:@tsci\/)?([^/.]+)[/.](.+)$/) + if (!match) { console.error( - "Invalid snippet path. Use format: author/snippetName, author.snippetName or @tsci/author.snippetName", + "Invalid snippet path. Use format: author/snippetName, author.snippetName, or @tsci/author.snippetName", ) process.exit(1) } - const ky = getKy() + const [, author, snippetName] = match + console.log(`Cloning ${author}/${snippetName}...`) + const ky = getKy() + let packageFileList try { - console.log(`Cloning ${author}/${snippetName}...`) - - const packageFileList = await ky - .post<{ - package_files: Array<{ - package_file_id: string - package_release_id: string - file_path: string - created_at: string - }> - }>("package_files/list", { - json: { - package_name: `${author}/${snippetName}`, - use_latest_version: true, + packageFileList = await ky + .post<{ package_files: Array<{ file_path: string }> }>( + "package_files/list", + { + json: { + package_name: `${author}/${snippetName}`, + use_latest_version: true, + }, }, - }) + ) .json() + } catch (error) { + console.error( + "Failed to fetch package files:", + error instanceof Error ? error.message : error, + ) + process.exit(1) + } - // Create directory if it doesn't exist - const dirPath = `./${author}.${snippetName}` - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath) - } + const dirPath = path.resolve(`${author}.${snippetName}`) + fs.mkdirSync(dirPath, { recursive: true }) - // Download each file that doesn't start with dist/ - for (const fileInfo of packageFileList.package_files) { - const filePath = fileInfo.file_path.startsWith("/") - ? fileInfo.file_path.slice(1) - : fileInfo.file_path + for (const fileInfo of packageFileList.package_files) { + const filePath = fileInfo.file_path.replace(/^\/|dist\//g, "") + if (!filePath) continue - if (filePath.startsWith("dist/")) continue + const fullPath = path.join(dirPath, filePath) + fs.mkdirSync(path.dirname(fullPath), { recursive: true }) + try { const fileContent = await ky - .post<{ - package_file: { - content_text: string - } - }>("package_files/get", { - json: { - package_name: `${author}/${snippetName}`, - file_path: fileInfo.file_path, + .post<{ package_file: { content_text: string } }>( + "package_files/get", + { + json: { + package_name: `${author}/${snippetName}`, + file_path: fileInfo.file_path, + }, }, - }) + ) .json() - const fullPath = path.join(dirPath, filePath) - const dirName = path.dirname(fullPath) + let fileText = fileContent.package_file.content_text - // Create nested directories if they don't exist - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }) + // Ensure all .tsx files contain "import '@tscircuit/core';" + if ( + filePath.endsWith(".tsx") && + !fileText.includes("@tscircuit/core") + ) { + fileText = `import "@tscircuit/core";\n\n${fileText}` } - fs.writeFileSync(fullPath, fileContent.package_file.content_text) + fs.writeFileSync(fullPath, fileText) + } catch (error) { + console.warn( + `Skipping ${filePath} due to error:`, + error instanceof Error ? error.message : error, + ) } + } - const npmrcPath = path.join(dirPath, ".npmrc") - fs.writeFileSync(npmrcPath, "@tsci:registry=https://npm.tscircuit.com") - - // Generate tsconfig.json - generateTsConfig(dirPath) - - // Detect package manager and install dependencies - const packageManager = detectPackageManager() - console.log(`Detected package manager: ${packageManager}`) + fs.writeFileSync( + path.join(dirPath, ".npmrc"), + "@tsci:registry=https://npm.tscircuit.com", + ) - // Install deps using the detected package manager - const dependencies = "@types/react @tscircuit/core" - try { - console.log("Installing dependencies...") - const installCommand = - packageManager === "yarn" - ? `cd ${dirPath} && yarn add -D ${dependencies}` - : packageManager === "pnpm" - ? `cd ${dirPath} && pnpm add -D ${dependencies}` - : packageManager === "bun" - ? `cd ${dirPath} && bun add -D ${dependencies}` - : `cd ${dirPath} && npm install -D ${dependencies}` - execSync(installCommand, { stdio: "inherit" }) - console.log("Dependencies installed successfully.") - } catch (error) { - console.error("Failed to install dependencies:", error) - } + generateTsConfig(dirPath) + setupTsciProject(dirPath) - console.log(`Successfully cloned to ./${author}.${snippetName}/`) - console.log(`Run "cd ${dirPath} && tsci dev" to start developing.`) - } catch (error) { - if (error instanceof Error) { - console.error("Failed to clone snippet:", error.message) - } else { - console.error("Failed to clone snippet:", error) - } - process.exit(1) - } + console.log(`Successfully cloned to ${dirPath}/`) + console.log( + `Run "cd ${path.dirname(dirPath)} && tsci dev" to start developing.`, + ) }) } diff --git a/cli/init/register.ts b/cli/init/register.ts index ce761de..8d2fd56 100644 --- a/cli/init/register.ts +++ b/cli/init/register.ts @@ -1,136 +1,70 @@ import type { Command } from "commander" import * as fs from "node:fs" import * as path from "node:path" -import { execSync } from "node:child_process" - -// Detect the package manager being used in the project -const detectPackageManager = (): string => { - const userAgent = process.env.npm_config_user_agent || "" - if (userAgent.startsWith("yarn")) return "yarn" - if (userAgent.startsWith("pnpm")) return "pnpm" - if (userAgent.startsWith("bun")) return "bun" - - if (fs.existsSync("yarn.lock")) return "yarn" - if (fs.existsSync("pnpm-lock.yaml")) return "pnpm" - if (fs.existsSync("bun.lockb")) return "bun" - - return "npm" // Default to npm -} - -// Generate a React-compatible tsconfig.json -const generateTsConfig = (dir: string) => { - const tsconfigPath = path.join(dir, "tsconfig.json") - const tsconfigContent = JSON.stringify( - { - compilerOptions: { - target: "ES6", - module: "ESNext", - jsx: "react-jsx", - outDir: "dist", - strict: true, - esModuleInterop: true, - moduleResolution: "node", - skipLibCheck: true, - forceConsistentCasingInFileNames: true, - resolveJsonModule: true, - sourceMap: true, - allowSyntheticDefaultImports: true, - experimentalDecorators: true, - }, - }, - null, - 2, - ) - if (!fs.existsSync(tsconfigPath)) { - fs.writeFileSync(tsconfigPath, tsconfigContent.trimStart()) - console.log(`Created: ${tsconfigPath}`) - } else { - console.log(`Skipped: ${tsconfigPath} already exists`) - } -} +import { setupTsciProject } from "lib/shared/setup-tsci-packages" +import { generateTsConfig } from "lib/shared/generate-ts-config" +import { writeFileIfNotExists } from "lib/shared/write-file-if-not-exists" export const registerInit = (program: Command) => { program .command("init") - .description("Initialize a new TSCircuit project in the current directory") - .action(() => { - const currentDir = process.cwd() - const indexFilePath = path.join(currentDir, "index.tsx") - const npmrcFilePath = path.join(currentDir, ".npmrc") + .description( + "Initialize a new TSCircuit project in the specified directory (or current directory if none is provided)", + ) + .argument( + "[directory]", + "Directory name (optional, defaults to current directory)", + ) + .action((directory?: string) => { + const projectDir = directory + ? path.resolve(process.cwd(), directory) + : process.cwd() + + // Ensure the directory exists + fs.mkdirSync(projectDir, { recursive: true }) - // Content for index.tsx - const indexContent = ` -import "@tscircuit/core" + // Create essential project files + writeFileIfNotExists( + path.join(projectDir, "index.tsx"), + ` +import "@tscircuit/core"; export default () => ( - - + + ); -`.trim() +`, + ) - // Content for .npmrc - const npmrcContent = ` + writeFileIfNotExists( + path.join(projectDir, ".npmrc"), + ` @tsci:registry=https://npm.tscircuit.com -`.trim() - - // Create index.tsx if it doesn't exist - if (!fs.existsSync(indexFilePath)) { - fs.writeFileSync(indexFilePath, indexContent.trimStart()) - console.log(`Created: ${indexFilePath}`) - } else { - console.log(`Skipped: ${indexFilePath} already exists`) - } - - // Create .npmrc if it doesn't exist - if (!fs.existsSync(npmrcFilePath)) { - fs.writeFileSync(npmrcFilePath, npmrcContent.trimStart()) - console.log(`Created: ${npmrcFilePath}`) - } else { - console.log(`Skipped: ${npmrcFilePath} already exists`) - } - - // Detect the package manager - const packageManager = detectPackageManager() - console.log(`Detected package manager: ${packageManager}`) +`, + ) - // Install deps using the detected package manager - const dependencies = "@types/react @tscircuit/core" + // Setup project dependencies try { - console.log("Installing dependencies...") - const installCommand = - packageManager === "yarn" - ? `yarn add -D ${dependencies}` - : packageManager === "pnpm" - ? `pnpm add -D ${dependencies}` - : packageManager === "bun" - ? `bun add -D ${dependencies}` - : `npm install -D ${dependencies}` - execSync(installCommand, { stdio: "inherit" }) - console.log("Dependencies installed successfully.") + setupTsciProject(projectDir) } catch (error) { console.error("Failed to install dependencies:", error) + process.exit(1) } // Generate tsconfig.json - generateTsConfig(currentDir) + try { + generateTsConfig(projectDir) + } catch (error) { + console.error("Failed to generate tsconfig.json:", error) + process.exit(1) + } - console.log( - `Initialization complete. Run "tsci dev" to start developing.`, + console.info( + `🎉 Initialization complete! Run ${directory ? `"cd ${directory}" & ` : ""}"tsci dev" to start developing.`, ) + process.exit(0) }) } diff --git a/lib/shared/detect-pkg-manager.ts b/lib/shared/detect-pkg-manager.ts new file mode 100644 index 0000000..3bcbea8 --- /dev/null +++ b/lib/shared/detect-pkg-manager.ts @@ -0,0 +1,15 @@ +import fs from "fs" + +// Detect the package manager being used in the project +export const detectPackageManager = (): string => { + const userAgent = process.env.npm_config_user_agent || "" + if (userAgent.startsWith("yarn")) return "yarn" + if (userAgent.startsWith("pnpm")) return "pnpm" + if (userAgent.startsWith("bun")) return "bun" + + if (fs.existsSync("yarn.lock")) return "yarn" + if (fs.existsSync("pnpm-lock.yaml")) return "pnpm" + if (fs.existsSync("bun.lockb")) return "bun" + + return "npm" // Default to npm +} diff --git a/lib/shared/generate-ts-config.ts b/lib/shared/generate-ts-config.ts new file mode 100644 index 0000000..c80318a --- /dev/null +++ b/lib/shared/generate-ts-config.ts @@ -0,0 +1,34 @@ +import path from "path" +import fs from "fs" + +// Generate a React-compatible tsconfig.json +export const generateTsConfig = (dir: string) => { + const tsconfigPath = path.join(dir, "tsconfig.json") + const tsconfigContent = JSON.stringify( + { + compilerOptions: { + target: "ES6", + module: "ESNext", + jsx: "react-jsx", + outDir: "dist", + strict: true, + esModuleInterop: true, + moduleResolution: "node", + skipLibCheck: true, + forceConsistentCasingInFileNames: true, + resolveJsonModule: true, + sourceMap: true, + allowSyntheticDefaultImports: true, + experimentalDecorators: true, + }, + }, + null, + 2, + ) + if (!fs.existsSync(tsconfigPath)) { + fs.writeFileSync(tsconfigPath, tsconfigContent.trimStart()) + console.log(`Created: ${tsconfigPath}`) + } else { + console.log(`Skipped: ${tsconfigPath} already exists`) + } +} diff --git a/lib/shared/setup-tsci-packages.ts b/lib/shared/setup-tsci-packages.ts new file mode 100644 index 0000000..13d2a72 --- /dev/null +++ b/lib/shared/setup-tsci-packages.ts @@ -0,0 +1,68 @@ +import { detectPackageManager } from "./detect-pkg-manager" +const fs = require("fs") +const path = require("path") +const { execSync } = require("child_process") + +/** + * Initializes a project in the specified directory, reads its name, and installs dependencies. + * + * @param {string} packageManager - Package manager to use (npm, yarn, pnpm, bun). + * @param {string[]} dependencies - List of dependencies to install. + * @param {string} directory - Directory where the project should be initialized. + */ +export function setupTsciProject( + directory = process.cwd(), + dependencies = ["@types/react", "@tscircuit/core"], +) { + const projectPath = path.resolve(directory) + if (!fs.existsSync(projectPath)) { + fs.mkdirSync(projectPath, { recursive: true }) + } + const packageManager = detectPackageManager() + + console.log(`Initializing project in ${projectPath}...`) + process.chdir(projectPath) + + if (!fs.existsSync("package.json")) { + const initCommand = + packageManager === "yarn" + ? "yarn init -y" + : packageManager === "pnpm" + ? "pnpm init" + : packageManager === "bun" + ? "bun init -y" + : "npm init -y" + + execSync(initCommand, { stdio: "inherit" }) + console.log("Project initialized successfully.") + } + + // Read and modify package.json + const packageJsonPath = path.join(projectPath, "package.json") + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) + + // Remove unwanted fields + delete packageJson.keywords + delete packageJson.author + delete packageJson.main + + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)) + console.log("Updated package.json to remove unnecessary fields.") + + if (dependencies.length > 0) { + console.log("Installing dependencies...") + const installCommand = + packageManager === "yarn" + ? `yarn add -D ${dependencies.join(" ")}` + : packageManager === "pnpm" + ? `pnpm add -D ${dependencies.join(" ")}` + : packageManager === "bun" + ? `bun add -D ${dependencies.join(" ")}` + : `npm install -D ${dependencies.join(" ")}` + + execSync(installCommand, { stdio: "inherit" }) + console.log("Dependencies installed successfully.") + } + + return packageJson.name || "unknown" +} diff --git a/lib/shared/write-file-if-not-exists.ts b/lib/shared/write-file-if-not-exists.ts new file mode 100644 index 0000000..d1a413f --- /dev/null +++ b/lib/shared/write-file-if-not-exists.ts @@ -0,0 +1,10 @@ +import fs from "fs" + +export const writeFileIfNotExists = (filePath: string, content: string) => { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, content.trimStart(), "utf-8") + console.info(`Created: ${filePath}`) + } else { + console.info(`Skipped: ${filePath} already exists`) + } +}