From b06f938ecdb62ae23ab9e8094d4a60071945b364 Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Tue, 17 May 2022 16:20:37 -0400 Subject: [PATCH 1/9] [Fixes #4] Add support for examples --- config.js | 6 ++++ index.js | 88 +++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 79 insertions(+), 15 deletions(-) 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..83cc10f 100755 --- a/index.js +++ b/index.js @@ -27,6 +27,19 @@ function prompt(question, defaultAnswer) { }); } +async function installDependencies() { + console.log(`Installing dependencies ...`); + await run(`cd ${dirName} && npm install`); +} + +async function initGit() { + 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"` + ); +} + /* --- Parse CLI Arguments */ const args = yargs(hideBin(process.argv)) @@ -35,6 +48,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,10 +61,13 @@ const args = yargs(hideBin(process.argv)) const starter = config.starters.find( (s) => s.name === (args.starter ?? config.defaults.starter.name) ); + +const timestamp = new Date().getTime(); + const dirName = args._[0] ?? `${config.defaults.dirName}-${nanoid(8).toLowerCase()}`; -/* --- New Project --- */ +/* --- New Project from Starter --- */ async function cloneStarter() { // Clone repo @@ -54,16 +75,9 @@ async function cloneStarter() { 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(); + await initGit(); // Output next steps: console.log(` @@ -75,6 +89,37 @@ Follow the instructions for getting Started here: `); } +/* --- New Project from Example --- */ + +async function cloneExample() { + const tmpDir = `examples-sparse-${timestamp}`; + console.log(`\nCreating new project in ${dirName} ...`); + + // 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(); + await initGit(); + + // 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 +141,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(); From ba6369492ce398e7883015515dd6f9f902b94aa2 Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Tue, 17 May 2022 16:24:58 -0400 Subject: [PATCH 2/9] Add a cleanup step --- index.js | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 83cc10f..2c3786b 100755 --- a/index.js +++ b/index.js @@ -95,20 +95,27 @@ async function cloneExample() { const tmpDir = `examples-sparse-${timestamp}`; console.log(`\nCreating new project in ${dirName} ...`); - // 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(); - await initGit(); + 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(); + await initGit(); + } 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(` From 516d92e749c76d6529bffaa6ddb1a328fb39ee30 Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Tue, 17 May 2022 16:29:38 -0400 Subject: [PATCH 3/9] Use timestamp for unique dir name --- index.js | 7 +++---- package-lock.json | 17 ----------------- package.json | 1 - 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index 2c3786b..798fc37 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"; @@ -62,10 +61,10 @@ const starter = config.starters.find( (s) => s.name === (args.starter ?? config.defaults.starter.name) ); -const timestamp = new Date().getTime(); +// Current time in seconds. +const timestamp = Math.round(new Date().getTime() / 1000); -const dirName = - args._[0] ?? `${config.defaults.dirName}-${nanoid(8).toLowerCase()}`; +const dirName = args._[0] ?? `${config.defaults.dirName}-${timestamp}`; /* --- New Project from Starter --- */ diff --git a/package-lock.json b/package-lock.json index 4982105..a7d280c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "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..6142eda 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "type": "module", "dependencies": { "chalk": "^5.0.0", - "nanoid": "^3.3.4", "yargs": "^17.3.1" } } From d6ea96b36eac482540c6e077db50f5d79299bbc1 Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Tue, 17 May 2022 16:35:47 -0400 Subject: [PATCH 4/9] Use example name as the dirName Starters behave the same --- index.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 798fc37..a797ec8 100755 --- a/index.js +++ b/index.js @@ -26,12 +26,12 @@ function prompt(question, defaultAnswer) { }); } -async function installDependencies() { +async function installDependencies(dirName) { console.log(`Installing dependencies ...`); await run(`cd ${dirName} && npm install`); } -async function initGit() { +async function initGit(dirName) { console.log(`Setting up Git ...`); await run(`rm -rf ${dirName}/.git`); await run( @@ -64,19 +64,20 @@ const starter = config.starters.find( // Current time in seconds. const timestamp = Math.round(new Date().getTime() / 1000); -const dirName = args._[0] ?? `${config.defaults.dirName}-${timestamp}`; - /* --- New Project from Starter --- */ async function cloneStarter() { + // Set references + const dirName = args._[0] ?? `${config.defaults.dirName}-${timestamp}`; + // Clone repo const cloneCommand = `git clone --depth=1 ${starter.repoUrl} ${dirName}`; console.log(`\nCreating new project in ${dirName} ...`); await run(cloneCommand); // Project Setup - await installDependencies(); - await initGit(); + await installDependencies(dirName); + await initGit(dirName); // Output next steps: console.log(` @@ -91,7 +92,8 @@ Follow the instructions for getting Started here: /* --- New Project from Example --- */ async function cloneExample() { - const tmpDir = `examples-sparse-${timestamp}`; + const dirName = args._[0] ?? `${args.example}-${timestamp}`; + const tmpDir = `__tmp${timestamp}__`; console.log(`\nCreating new project in ${dirName} ...`); try { @@ -107,8 +109,8 @@ async function cloneExample() { await run(`rm -rf ${tmpDir}`); // Project Setup - await installDependencies(); - await initGit(); + await installDependencies(dirName); + await initGit(dirName); } catch (err) { console.error(err); if (fs.existsSync(dirName)) await run(`rm -rf ${dirName}`); From 07476c6fb9bb6ca6e24a2135cbd9d449ad6867f3 Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Tue, 17 May 2022 16:39:31 -0400 Subject: [PATCH 5/9] [Fixes #10] Check for dirName before prepending timestamp --- index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index a797ec8..6ccdf50 100755 --- a/index.js +++ b/index.js @@ -26,6 +26,12 @@ 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`); @@ -68,7 +74,7 @@ const timestamp = Math.round(new Date().getTime() / 1000); async function cloneStarter() { // Set references - const dirName = args._[0] ?? `${config.defaults.dirName}-${timestamp}`; + const dirName = getDirName(config.defaults.dirName); // Clone repo const cloneCommand = `git clone --depth=1 ${starter.repoUrl} ${dirName}`; @@ -92,7 +98,7 @@ Follow the instructions for getting Started here: /* --- New Project from Example --- */ async function cloneExample() { - const dirName = args._[0] ?? `${args.example}-${timestamp}`; + const dirName = getDirName(args.example); const tmpDir = `__tmp${timestamp}__`; console.log(`\nCreating new project in ${dirName} ...`); From b3480ffe3db138389135801fa2b845de5a1f57f0 Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Tue, 17 May 2022 16:40:02 -0400 Subject: [PATCH 6/9] Bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7d280c..6ee6b4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "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", diff --git a/package.json b/package.json index 6142eda..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": { From 5eb5b43bdfbf3e47f1c4cf0234095b17ea0086ae Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Tue, 17 May 2022 16:45:17 -0400 Subject: [PATCH 7/9] Update usage instructions --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bed12c7..1c39ada 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,16 @@ 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 + +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). + ### 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 +51,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 From 513cedec226a328a9d4dea62865f16283456f135 Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Mon, 23 May 2022 11:57:37 -0400 Subject: [PATCH 8/9] Check git version before cloning example --- index.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/index.js b/index.js index 6ccdf50..3ee2c52 100755 --- a/index.js +++ b/index.js @@ -45,6 +45,36 @@ async function initGit(dirName) { ); } +/** + * 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)) @@ -98,6 +128,24 @@ 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 to ${chalk.underline("support@stackbit.com")}.` + ); + 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} ...`); From c22417dc72ce7cc33d321bc961b560e2d8180129 Mon Sep 17 00:00:00 2001 From: Sean C Davis Date: Mon, 23 May 2022 11:59:47 -0400 Subject: [PATCH 9/9] Note examples as experimental --- README.md | 4 +++- index.js | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c39ada..bc7d2e7 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ 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 +### 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. @@ -43,6 +43,8 @@ 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: diff --git a/index.js b/index.js index 3ee2c52..a91c0ae 100755 --- a/index.js +++ b/index.js @@ -133,7 +133,10 @@ async function cloneExample() { if (!gitVersionMatch || !gitVersionMatch[0]) { console.error( `Cannot determine git version, which is required for starting from an example.`, - `\nPlease report this to ${chalk.underline("support@stackbit.com")}.` + `\nPlease report this:`, + chalk.underline( + "https://github.com/stackbit/create-stackbit-app/issues/new" + ) ); process.exit(1); }