Skip to content

v0.1.1: Examples monorepo #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
```

Expand All @@ -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:
Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
159 changes: 141 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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))
Expand All @@ -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();

Expand All @@ -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(`
Expand All @@ -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() {
Expand All @@ -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();
21 changes: 2 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -15,7 +15,6 @@
"type": "module",
"dependencies": {
"chalk": "^5.0.0",
"nanoid": "^3.3.4",
"yargs": "^17.3.1"
}
}