Skip to content
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

feat: basic create-frames cli tool #143

Merged
merged 4 commits into from
Mar 26, 2024
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
5 changes: 5 additions & 0 deletions .changeset/smooth-bugs-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-frames": minor
---

feat: introduce cli tool to bootstrap projects from templates
3 changes: 3 additions & 0 deletions .scripts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
33 changes: 33 additions & 0 deletions .scripts/prepare-create-frames.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* This script is used to prepare create-frames package for publishing.
*
* It copies the templates directory to the root of the package so that it can be used by the create-frames package.
*/

import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import glob from "fast-glob";
import { cpSync, renameSync, rmSync } from 'node:fs';

const __dirname = dirname(fileURLToPath(import.meta.url));


rmSync(resolve(__dirname, "../packages/create-frames/templates"), { force: true, recursive: true });

cpSync(
resolve(__dirname, "../templates"),
resolve(__dirname, "../packages/create-frames/templates"),
{
recursive: true,
filter: (src) => !/(node_modules|\.turbo|\.next|next-env\.d\.ts)/.test(src),
}
);

const dotfiles = await glob(
resolve(__dirname, "../packages/create-frames/templates/**/.*")
);

// rename dotfiles to use underscore instead of dot so we can publish them to npm as is
for (const doftile of dotfiles) {
renameSync(doftile, doftile.replace("/.", "/_"));
}
33 changes: 27 additions & 6 deletions docs/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import { HomePage } from "vocs/components";
<HomePage.Button href="https://github.com/framesjs/frames.js">
GitHub
</HomePage.Button>
<HomePage.Button href="#quickstart" variant="accent">
Quickstart
</HomePage.Button>
<HomePage.Button href="https://github.com/framesjs/frames.js">
GitHub
</HomePage.Button>
</HomePage.Buttons>
</HomePage.Root>

Expand All @@ -31,15 +37,25 @@ import { HomePage } from "vocs/components";

# Quickstart

## 1. Clone the frames.js starter template (with local debugger)
## Bootstrap the project from template using the CLI tool

Run to clone the starter into a new folder called `framesjs-starter`
Run one of the commands below based on your preferred package manager and then follow the steps in the terminal.

```bash
npx degit github:framesjs/frames.js/examples/framesjs-starter#main framesjs-starter
:::code-group

```bash [npm]
npm init frames
```

```bash [yarn]
yarn create frames
```

```bash [pnpm]
pnpm create frames
```

or [clone from github](https://github.com/framesjs/frames.js/tree/main/examples/framesjs-starter)
:::

## Alternatively, add frames.js to your existing project manually

Expand All @@ -58,7 +74,12 @@ export async function generateMetadata() {
return {
title: "My page",
other: await fetchMetadata(
new URL("/frames", process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000")
new URL(
"/frames",
process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000"
)
),
};
}
Expand Down
6 changes: 2 additions & 4 deletions examples/framesjs-starter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
"dev:monorepo": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"update-debugger": "npx degit github:framesjs/frames.js/examples/framesjs-starter/app/debug#main app/debug --force",
"update-framesjs": "yarn upgrade frames.js@latest && yarn run update-debugger"
"lint": "next lint"
},
"dependencies": {
"@farcaster/core": "^0.14.3",
Expand Down Expand Up @@ -43,4 +41,4 @@
"tailwindcss": "^3.3.0",
"typescript": "^5.3.3"
}
}
}
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"name": "framesjs-monorepo",
"private": true,
"scripts": {
"build": "turbo build",
"build:ci": "turbo build --filter=!debugger --filter=!framesjs-starter --filter=!utils-starter --filter=!docs",
"build": "turbo build --filter=!./templates/* && node ./.scripts/prepare-create-frames.js",
"build:ci": "turbo build --filter=!debugger --filter=!framesjs-starter --filter=!utils-starter --filter=!docs --filter=!template-*",
"dev": "FJS_MONOREPO=true turbo dev --filter=framesjs-starter... --filter=debugger...",
"dev:custom-redirects": "turbo dev --filter=custom-redirects...",
"dev:utils-starter": "turbo dev --filter=utils-starter...",
"dev:all": "turbo dev",
"lint": "turbo lint",
"lint": "turbo lint --filter=!template-*",
"test:ci": "jest --ci",
"test": "cd ./packages/frames.js && npm run test:watch",
"publish-packages": "turbo run build lint && changeset version && changeset publish && git push --follow-tags origin main",
Expand All @@ -23,9 +23,11 @@
"@framesjs/typescript-config": "*",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.11",
"fast-glob": "^3.3.2",
"jest": "^29.7.0",
"nock": "beta",
"prettier": "^3.1.1",
"rimraf": "^5.0.5",
"turbo": "^1.12.2"
},
"engines": {
Expand All @@ -35,7 +37,8 @@
"workspaces": [
"docs",
"examples/*",
"packages/*"
"packages/*",
"templates/*"
],
"version": "0.3.0-canary.0"
}
1 change: 1 addition & 0 deletions packages/create-frames/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
templates
1 change: 1 addition & 0 deletions packages/create-frames/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!templates
48 changes: 48 additions & 0 deletions packages/create-frames/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Create Frames.js app

Create a new Frames.js app with a single command using predefined templates.
michalkvasnicak marked this conversation as resolved.
Show resolved Hide resolved

## Creating a project

### Using npm

```sh
npm init frames
```

### Using yarn

```sh
yarn create frames
```

### Using pnpm

```sh
pnpm create frames
```

## Installing globally (optional) and running from terminal

You can also install this package globally and run it from your terminal.

### Using npm

```sh
npm install -g create-frames
create-frames
```

### Using yarn

```sh
yarn global add create-frames
create-frames
```

### Using pnpm

```sh
pnpm add -g create-frames
create-frames
```
30 changes: 30 additions & 0 deletions packages/create-frames/bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { getTemplates } from "./utils/getTemplates.js";
import { create } from "./create.js";

// use only default command
yargs(hideBin(process.argv))
.scriptName("create-frames")
.usage("Usage: $0 [options]")
.command(
"$0",
"Create a new project",
{
name: {
alias: "n",
describe: "Name of the project",
type: "string",
},
template: {
alias: "t",
describe: "Choose a template for the project",
choices: getTemplates(),
},
},
(args) => {
create(args);
}
)
.parse();
135 changes: 135 additions & 0 deletions packages/create-frames/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { intro, log, outro, select, confirm, text } from "@clack/prompts";
import { getTemplates } from "./utils/getTemplates.js";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import ignore from "ignore";
import pc from "picocolors";
import { detect as detectPackageManager } from "detect-package-manager";
import {
cpSync,
readFileSync,
readdirSync,
renameSync,
writeFileSync,
} from "node:fs";
import { packageManagerRunCommand } from "./utils/packageManagerRunCommand.js";

const __dirname = dirname(fileURLToPath(import.meta.url));

/**
*
* @param {import('yargs').ArgumentsCamelCase<{ t?: string, n?: string }>} params
*/
export async function create(params) {
if (params.$0 !== "create-frames") {
throw new Error("Invalid command");
}

intro("Welcome to frames.js");

// if there is no -n argument (name, ask for the name of new project)
let projectName =
params.n ||
(await text({
message: "Enter the name of your project",
placeholder: "my-frames-app",
validate(input) {
if (!input) {
return "Project name is required";
}

return;
},
}));

const destDir = resolve(process.cwd(), projectName);

const template =
params.t ||
(await select({
message: "Choose a template for the project",
options: getTemplates().map((template) => ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be great to have the existing templates here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that way you would need to update the list manually each time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to keep this as is otherwise it could easily happen that you forget to add a template here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok now I think I understand what you mean. Moving templates from starter to standalone templates.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added 2 more templates based on starters from examples folder. Would you like to get rid of examples altogether? I think that would be better to do in different PR.

name: template,
value: template,
})),
defaultValue: "next",
validate(input) {
if (!input) {
return "Template is required";
}

return;
},
}));

const templateDir = resolve(__dirname, "./templates", template);
const ignoredPatterns = readFileSync(
resolve(templateDir, "_gitignore"),
"utf-8"
);
const ignored = ignore().add(ignoredPatterns);

cpSync(templateDir, destDir, {
force: true,
recursive: true,
filter(src) {
const path = relative(templateDir, src);

return !path || !ignored.ignores(path);
},
});

for (const file of readdirSync(destDir)) {
if (!file.startsWith("_")) continue;

renameSync(
resolve(destDir, file),
resolve(destDir, file.replace("_", "."))
);
}

const pkgJsonPath = resolve(destDir, "package.json");
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
pkgJson.name = projectName;
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));

log.success(`Project successfully scaffolded in ${pc.blue(destDir)}!`);

const pkgManager = await detectPackageManager();

const wantsToInstallDependencies = await confirm({
message: `Do you want to install the dependencies using ${pkgManager}?`,
initialValue: true,
});

if (wantsToInstallDependencies) {
log.message(`Installing the dependencies...`);
const result = spawnSync(pkgManager, ["install"], {
cwd: destDir,
stdio: "ignore",
});

if (result.status !== 0) {
log.error(
`Failed to install the dependencies, please install them manually.`
);
process.exit(1);
}

log.success(`Dependencies installed!`);
}

log.message("Next steps:");
log.step(
`1. Go to the project directory by running: ${pc.blue(`cd ./${projectName}`)}`
);
log.step(
`2. Start the development server and run the app in debugger by running: ${pc.blue(await packageManagerRunCommand("dev"))}`
);
log.step(
`3. Open your browser and go to ${pc.blue(`http://localhost:3010`)} to see your app running in the debugger`
);

outro("Done! Your project has been set up! 🎉");
}
Loading
Loading