diff --git a/README.md b/README.md index bed12c7..bc7d2e7 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ To see a full list of options use the `--help` flag: Options: --version Show version number [boolean] - -s, --starter Choose a starter [choices: "nextjs", "ts-nextjs"] + -s, --starter Choose a starter [choices: "nextjs", "ts-nextjs"] + -e, --example Start from an example + [choices: "algolia-search", "dynamic-app", "sb-countdown", "sb-typist"] --help Show help [boolean] ``` @@ -31,6 +33,18 @@ npx create-stackbit-app --starter ts-nextjs If no starter option is provided, [the default starter](https://github.com/stackbit-themes/nextjs-starter) is used. +### Starting from an Example (๐Ÿงช Experimental) + +Use the `--example` option to start a project from an example. Run the command with the `--help` flag to see a full list of available starters. + +```txt +npx create-stackbit-app --example algolia-search +``` + +This will create a new project matching the name of the example, unless overridden (see below). [See here for a full list of starters](https://github.com/stackbit-themes/stackbit-examples). + +_Note: This is an experimental feature. Please [report issues](https://github.com/stackbit/create-stackbit-app/issues/new)._ + ### Setting Project Directory Pass a directory name as the only argument when running the command. For example, if you wanted your directory to be name `my-site`, the command would look something like this: @@ -39,7 +53,7 @@ Pass a directory name as the only argument when running the command. For example npx create-stackbit-app my-site ``` -If no name is provided, the directory will be `my-stackbit-site-[id]`, where `[id]` is a randomly-generated string used to avoid directory conflicts. +If no name is provided, the directory will be `my-stackbit-site` for starters or will match the name of the example if starting from an example. If the directory already exists, a timestamp value will be appended to the directory name to ensure uniqueness. ## Adding Stackbit to Existing Projects diff --git a/config.js b/config.js index aa092e7..0786875 100644 --- a/config.js +++ b/config.js @@ -9,7 +9,13 @@ const starters = [ }, ]; +const examples = { + repoUrl: "https://github.com/stackbit-themes/stackbit-examples", + directories: ["algolia-search", "dynamic-app", "sb-countdown", "sb-typist"], +}; + export default { defaults: { dirName: "my-stackbit-site", starter: starters[0] }, + examples, starters, }; diff --git a/index.js b/index.js index 561d712..a91c0ae 100755 --- a/index.js +++ b/index.js @@ -3,7 +3,6 @@ import chalk from "chalk"; import { exec } from "child_process"; import fs from "fs"; -import { nanoid } from "nanoid"; import path from "path"; import readline from "readline"; import util from "util"; @@ -27,6 +26,55 @@ function prompt(question, defaultAnswer) { }); } +function getDirName(defaultDirName) { + let dirName = args._[0] ?? defaultDirName; + if (fs.existsSync(dirName)) dirName += `-${timestamp}`; + return dirName; +} + +async function installDependencies(dirName) { + console.log(`Installing dependencies ...`); + await run(`cd ${dirName} && npm install`); +} + +async function initGit(dirName) { + console.log(`Setting up Git ...`); + await run(`rm -rf ${dirName}/.git`); + await run( + `cd ${dirName} && git init && git add . && git commit -m "New Stackbit project"` + ); +} + +/** + * Given a version string, compare it to a control version. Returns: + * + * -1: version is less than (older) than control + * 0: version and control are identical + * 1: version is greater than (newer) than control + * + * @param {string} version Version string that is being compared + * @param {string} control Version that is being compared against + */ +function compareVersion(version, control) { + // References + let returnValue = 0; + // Return 0 if the versions match. + if (version === control) return returnValue; + // Break the versions into arrays of integers. + const getVersionParts = (str) => str.split(".").map((v) => parseInt(v)); + const versionParts = getVersionParts(version); + const controlParts = getVersionParts(control); + // Loop and compare each item. + controlParts.every((controlPart, idx) => { + // If the versions are equal at this part, we move on to the next part. + if (versionParts[idx] === controlPart) return true; + // Otherwise, set the return value, then break out of the loop. + returnValue = versionParts[idx] > controlPart ? 1 : -1; + return false; + }); + return returnValue; +} + /* --- Parse CLI Arguments */ const args = yargs(hideBin(process.argv)) @@ -35,6 +83,11 @@ const args = yargs(hideBin(process.argv)) describe: "Choose a starter", choices: config.starters.map((s) => s.name), }) + .option("example", { + alias: "e", + describe: "Start from an example", + choices: config.examples.directories, + }) .help() .parse(); @@ -43,27 +96,24 @@ const args = yargs(hideBin(process.argv)) const starter = config.starters.find( (s) => s.name === (args.starter ?? config.defaults.starter.name) ); -const dirName = - args._[0] ?? `${config.defaults.dirName}-${nanoid(8).toLowerCase()}`; -/* --- New Project --- */ +// Current time in seconds. +const timestamp = Math.round(new Date().getTime() / 1000); + +/* --- New Project from Starter --- */ async function cloneStarter() { + // Set references + const dirName = getDirName(config.defaults.dirName); + // Clone repo const cloneCommand = `git clone --depth=1 ${starter.repoUrl} ${dirName}`; console.log(`\nCreating new project in ${dirName} ...`); await run(cloneCommand); - // Install dependencies - console.log(`Installing dependencies ...`); - await run(`cd ${dirName} && npm install`); - - // Set up git - console.log(`Setting up Git ...`); - await run(`rm -rf ${dirName}/.git`); - await run( - `cd ${dirName} && git init && git add . && git commit -m "New Stackbit project"` - ); + // Project Setup + await installDependencies(dirName); + await initGit(dirName); // Output next steps: console.log(` @@ -75,6 +125,66 @@ Follow the instructions for getting Started here: `); } +/* --- New Project from Example --- */ + +async function cloneExample() { + const gitResult = await run("git --version"); + const gitVersionMatch = gitResult.stdout.match(/\d+\.\d+\.\d+/); + if (!gitVersionMatch || !gitVersionMatch[0]) { + console.error( + `Cannot determine git version, which is required for starting from an example.`, + `\nPlease report this:`, + chalk.underline( + "https://github.com/stackbit/create-stackbit-app/issues/new" + ) + ); + process.exit(1); + } + const minGitVersion = "2.25.0"; + if (compareVersion(gitVersionMatch[0], minGitVersion) < 0) { + console.error( + `Starting from an example requires git version ${minGitVersion} or later.`, + "Please upgrade" + ); + process.exit(1); + } + + const dirName = getDirName(args.example); + const tmpDir = `__tmp${timestamp}__`; + console.log(`\nCreating new project in ${dirName} ...`); + + try { + // Sparse clone the monorepo. + await run( + `git clone --depth 1 --filter=blob:none --sparse ${config.examples.repoUrl} ${tmpDir}` + ); + // Checkout just the example dir. + await run(`cd ${tmpDir} && git sparse-checkout set ${args.example}`); + // Copy out into a new directory within current working directory. + await run(`cp -R ${tmpDir}/${args.example} ${dirName}`); + // Delete the clone. + await run(`rm -rf ${tmpDir}`); + + // Project Setup + await installDependencies(dirName); + await initGit(dirName); + } catch (err) { + console.error(err); + if (fs.existsSync(dirName)) await run(`rm -rf ${dirName}`); + if (fs.existsSync(tmpDir)) await run(`rm -rf ${tmpDir}`); + process.exit(1); + } + + // Output next steps: + console.log(` +๐ŸŽ‰ ${chalk.bold("Your example project is ready!")} ๐ŸŽ‰ + +Follow the instructions and learn more about the example here: + + ${config.examples.repoUrl}/tree/main/${args.example}#readme + `); +} + /* --- Existing Project --- */ async function integrateStackbit() { @@ -96,9 +206,22 @@ Visit the following URL to learn more about the integration process: /* --- Run --- */ -const packageJsonFilePath = path.join(process.cwd(), "package.json"); -const hasPackageJson = fs.existsSync(packageJsonFilePath); -const runFunc = hasPackageJson ? integrateStackbit : cloneStarter; -await runFunc(); +async function doCreate() { + // If the current directory has a package.json file, we assume we're in an + // active project, and will not create a new project. + const packageJsonFilePath = path.join(process.cwd(), "package.json"); + if (fs.existsSync(packageJsonFilePath)) return integrateStackbit(); + // If both starter and example were specified, throw an error message. + if (args.starter && args.example) { + console.error("[ERROR] Cannot specify a starter and an example."); + process.exit(1); + } + // Start from an example if specified. + if (args.example) return cloneExample(); + // Otherwise, use a starter, which falls back to the default if not set. + return cloneStarter(); +} + +await doCreate(); rl.close(); diff --git a/package-lock.json b/package-lock.json index 4982105..6ee6b4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "create-stackbit-app", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "create-stackbit-app", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "dependencies": { "chalk": "^5.0.0", - "nanoid": "^3.3.4", "yargs": "^17.3.1" }, "bin": { @@ -105,17 +104,6 @@ "node": ">=8" } }, - "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -260,11 +248,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index 0423ad0..b5f05f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-stackbit-app", - "version": "0.1.0", + "version": "0.1.1", "description": "Create a new Stackbit site, or add Stackbit to an existing site.", "main": "index.js", "scripts": { @@ -15,7 +15,6 @@ "type": "module", "dependencies": { "chalk": "^5.0.0", - "nanoid": "^3.3.4", "yargs": "^17.3.1" } }