diff --git a/package.json b/package.json index 4528cb21..708ff52c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mui-treasury", "description": "A CLI to clone Material UI blocks into your project.", "author": "siriwatknp", - "version": "0.1.7", + "version": "0.2.0", "homepage": "https://siriwatknp.github.io/mui-treasury/", "license": "MIT", "publishConfig": { @@ -38,10 +38,11 @@ "@types/tar": "4.0.4", "chalk": "4.1.0", "cmdk": "0.2.0", - "commander": "7.1.0", + "commander": "11.1.0", "cosmiconfig": "8.2.0", "cpy": "8.1.1", "got": "11.8.2", + "prompts": "2.4.2", "react-dropzone": "^14.2.3", "react-ga4": "2.1.0", "rimraf": "3.0.2", @@ -73,6 +74,7 @@ "@testing-library/user-event": "14.5.2", "@types/color": "3.0.6", "@types/jest": "29.5.3", + "@types/prompts": "2.4.9", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", "autoprefixer": "10.4.16", diff --git a/src/index.ts b/src/index.ts index 70c13e57..bf08a044 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,14 @@ #!/usr/bin/env node import { execSync } from "child_process"; import fs from "fs"; +import path from "path"; import { Stream } from "stream"; import { promisify } from "util"; import chalk from "chalk"; import { cosmiconfig } from "cosmiconfig"; import cpy from "cpy"; import got from "got"; +import prompts from "prompts"; import rimraf from "rimraf"; import * as tar from "tar"; import checkForUpdate from "update-check"; @@ -35,14 +37,6 @@ const CONFIG_FILE_TEMPLATE = `module.exports = { }; `; -const cloneParams: { - sources: string[]; - options: CloneOptions | undefined; -} = { - sources: [], - options: undefined, -}; - const logger = { log: (...text: string[]) => { console.log(chalk.bgHex("D4D4D8").hex("3F3F46")("mui-treasury"), ...text); @@ -161,9 +155,61 @@ async function notifyUpdate(): Promise { const program = createProgram({ commands: { - clone: (sources, options) => { - cloneParams.sources = sources; - cloneParams.options = options; + clone: async (sources, options) => { + const config = await getConfigFile(options); + if (config.dir && !config.dir.startsWith("/")) { + config.dir = `/${config.dir}`; + } + for (let field of Object.entries(config)) { + logger.config(`"${field[0]}: ${field[1]}"`); + } + const tempRoot = process.cwd() + "/mui-treasury-tmp"; + const tempTemplateRoot = process.cwd() + "/mui-treasury-template-tmp"; + const actualRoot = process.cwd() + config.dir; + if (!fs.existsSync(tempRoot)) { + fs.mkdirSync(tempRoot, { recursive: true }); + } + if (!fs.existsSync(tempTemplateRoot)) { + fs.mkdirSync(tempTemplateRoot, { recursive: true }); + } + logger.info(`start cloning ${chalk.bold(sources.length)} packages...`); + try { + if (sources.length) { + await downloadAndExtractCode(tempRoot, sources, config.branch); + } + const excludedFiles = [ + ...(!config.storybook ? [`!${tempRoot}/**/*.stories.*`] : []), + `!${tempRoot}/**/*.mdx`, + ]; + logger.info("finishing things up..."); + await Promise.all( + sources.map((mod) => + cpy( + [ + // default template is typescript (ts codes live in "src" folder) + `${tempRoot}/${mod}${ + TEMPLATE_FOLDER_MAP[config.template] + ? `/${TEMPLATE_FOLDER_MAP[config.template]}` + : "" + }/*`, + ...excludedFiles, + ], + `${actualRoot}/${mod}`, + { + overwrite: true, + } + ) + ) + ); + } catch (error) { + logger.log(chalk.bold(chalk.red("❌ clone failed!"))); + throw error; + } finally { + // clean up temp folder + await Promise.all([removeDir(tempRoot), removeDir(tempTemplateRoot)]); + } + logger.log(chalk.bold(chalk.green("✅ cloned successfully!"))); + await notifyUpdate(); }, init: () => { fs.writeFile( @@ -175,72 +221,126 @@ const program = createProgram({ } ); }, + create: async (template, directory, options = {}) => { + const { branch = "master" } = options; + if (!template) { + const { template: selectedTemplate } = await prompts({ + name: "template", + type: "autocomplete", + message: "Pick a template", + suggest: (input, choices) => + Promise.resolve( + choices.filter( + (i) => + i.title.toLowerCase().includes(input.toLowerCase()) || + i.value.toLowerCase().includes(input.toLowerCase()) + ) + ), + choices: [ + { + title: "[TS] Material UI - Next.js App Router", + value: "material-ui-nextjs-ts", + }, + { + title: "[TS] Material UI - Next.js Pages Router", + value: "material-ui-nextjs-pages-router-ts", + }, + { + title: "[TS] Material UI - Remix", + value: "material-ui-remix-ts", + }, + { + title: "[TS] Material UI - Vite", + value: "material-ui-vite-ts", + }, + { + title: "[TS] Base UI - Next.js Tailwind", + value: "base-ui-nextjs-tailwind-ts", + }, + { + title: "[TS] Base UI - Vite Tailwind", + value: "base-ui-vite-tailwind-ts", + }, + { + title: "[TS] Joy UI - Next.js", + value: "joy-ui-nextjs-ts", + }, + { title: "[TS] Joy UI - Vite", value: "joy-ui-vite-ts" }, + { + title: "[TS] Material UI - Next.js v4 to v5 Migration", + value: "material-ui-nextjs-ts-v4-v5-migration", + }, + { + title: "[TS] Material UI - CRA", + value: "material-ui-cra-ts", + }, + { + title: "[TS] Material UI - CRA, styled-components", + value: "material-ui-cra-styled-components-ts", + }, + { + title: "[TS] Material UI - CRA, Tailwind", + value: "material-ui-cra-tailwind-ts", + }, + { title: "[TS] Base UI - CRA", value: "base-ui-cra-ts" }, + { title: "[TS] Joy UI - CRA", value: "joy-ui-cra-ts" }, + { + title: "Material UI - CRA, styled-components", + value: "material-ui-cra-styled-components", + }, + { + title: "Material UI - Express SSR", + value: "material-ui-express-ssr", + }, + { title: "Material UI - Gatsby", value: "material-ui-gatsby" }, + { title: "Material UI - Next.js", value: "material-ui-nextjs" }, + { + title: "Material UI - Next.js Pages Router", + value: "material-ui-nextjs-pages-router", + }, + { title: "Material UI - Preact", value: "material-ui-preact" }, + { title: "Material UI - via CDN", value: "material-ui-via-cdn" }, + { title: "Material UI - Vite", value: "material-ui-vite" }, + { title: "Material UI - CRA", value: "material-ui-cra" }, + { title: "Base UI - CRA", value: "base-ui-cra" }, + { + title: "Base UI - Vite Tailwind", + value: "base-ui-vite-tailwind", + }, + ], + }); + template = selectedTemplate; + } + if (!directory) { + const { dir } = await prompts({ + name: "dir", + type: "text", + message: "Type a folder name", + initial: template, + }); + directory = dir; + } + logger.info(`⏳ pulling the template to ${chalk.bold(directory)}`); + const root = path.resolve(process.cwd(), directory); + fs.mkdirSync(root, { recursive: true }); + try { + await pipeline( + got.stream( + `https://codeload.github.com/mui/material-ui/tar.gz/${branch}` + ), + tar.extract({ cwd: root, strip: 3 }, [ + `material-ui-${branch}/examples/${template}`, + ]) + ); + } catch (error) { + logger.log(chalk.bold(chalk.red("❌ clone failed!"))); + throw error; + } + logger.log(chalk.bold(chalk.green("✅ created successfully!"))); + logger.log(`👉 "cd ${directory}" and install the dependencies`); + await notifyUpdate(); + }, }, }); program.parse(process.argv); - -async function runCloneCommand() { - const config = await getConfigFile(cloneParams.options); - if (config.dir && !config.dir.startsWith("/")) { - config.dir = `/${config.dir}`; - } - for (let field of Object.entries(config)) { - logger.config(`"${field[0]}: ${field[1]}"`); - } - const tempRoot = process.cwd() + "/mui-treasury-tmp"; - const tempTemplateRoot = process.cwd() + "/mui-treasury-template-tmp"; - const actualRoot = process.cwd() + config.dir; - if (!fs.existsSync(tempRoot)) { - fs.mkdirSync(tempRoot, { recursive: true }); - } - if (!fs.existsSync(tempTemplateRoot)) { - fs.mkdirSync(tempTemplateRoot, { recursive: true }); - } - const sources = cloneParams.sources; - logger.info(`start cloning ${chalk.bold(sources.length)} packages...`); - try { - if (sources.length) { - await downloadAndExtractCode(tempRoot, sources, config.branch); - } - const excludedFiles = [ - ...(!config.storybook ? [`!${tempRoot}/**/*.stories.*`] : []), - `!${tempRoot}/**/*.mdx`, - ]; - logger.info("finishing things up..."); - await Promise.all( - sources.map((mod) => - cpy( - [ - // default template is typescript (ts codes live in "src" folder) - `${tempRoot}/${mod}${ - TEMPLATE_FOLDER_MAP[config.template] - ? `/${TEMPLATE_FOLDER_MAP[config.template]}` - : "" - }/*`, - ...excludedFiles, - ], - `${actualRoot}/${mod}`, - { - overwrite: true, - } - ) - ) - ); - } catch (error) { - logger.log(chalk.bold(chalk.red("❌ clone failed!"))); - throw error; - } finally { - // clean up temp folder - await Promise.all([removeDir(tempRoot), removeDir(tempTemplateRoot)]); - } - logger.log(chalk.bold(chalk.green("✅ cloned successfully!"))); -} - -if (cloneParams.sources.length) { - runCloneCommand() - .then(notifyUpdate) - .catch((error) => { - throw error; - }); -} diff --git a/src/program.ts b/src/program.ts index aa96558a..b1adf47b 100644 --- a/src/program.ts +++ b/src/program.ts @@ -1,4 +1,4 @@ -import commander from "commander"; +import * as commander from "commander"; // @ts-ignore import packageJson from "../package.json"; @@ -16,6 +16,11 @@ export type Params = { options: CloneOptions, command: any ) => void | Promise; + create?: ( + source: string, + destination: string, + options: any + ) => void | Promise; }; }; @@ -28,7 +33,9 @@ function parseTemplate(value: string) { return value; } -export const createProgram = ({ commands: { clone, init } }: Params) => { +export const createProgram = ({ + commands: { clone, init, create }, +}: Params) => { const program = new commander.Command(packageJson.name).version( packageJson.version, "-v, --version", @@ -51,8 +58,25 @@ export const createProgram = ({ commands: { clone, init } }: Params) => { ) .option("-b, --branch [branch]", "target branch on github") .option("--storybook", "storybook file(s) will be included.") - .action((sources, options, command) => { - clone?.(sources, options, command); + .action(async (sources, options, command) => { + await clone?.(sources, options, command); + }); + + program.command("init").action(() => { + init?.(); + }); + + program + .command("new") + .description("create a new project from a template") + .argument("[template]", "the source template") + .argument( + "[directory]", + "the destination folder to clone the template into" + ) + .option("-b, --branch [branch]", "target branch on github") + .action(async (template, directory, options) => { + await create?.(template, directory, options); }); return program; diff --git a/yarn.lock b/yarn.lock index 4db52af2..35b11332 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3880,6 +3880,14 @@ resolved "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz" integrity sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ== +"@types/prompts@2.4.9": + version "2.4.9" + resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.4.9.tgz#8775a31e40ad227af511aa0d7f19a044ccbd371e" + integrity sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA== + dependencies: + "@types/node" "*" + kleur "^3.0.3" + "@types/prop-types@*": version "15.7.8" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz" @@ -5368,10 +5376,10 @@ command-score@0.1.2: resolved "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz" integrity sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w== -commander@7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz" - integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== +commander@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== commander@^2.19.0, commander@^2.20.0: version "2.20.3" @@ -10404,9 +10412,9 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prompts@^2.0.1, prompts@^2.4.0: +prompts@2.4.2, prompts@^2.0.1, prompts@^2.4.0: version "2.4.2" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3"