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

Add create-yoga-app for - feature #1387

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions packages/create-yoga/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
node_modules/
210 changes: 210 additions & 0 deletions packages/create-yoga/create-yoga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import chalk from "chalk";
import { mkdir } from "fs/promises";
import path from "path";
import retry from "async-retry";
import { isWriteAccess } from "./utils/checkFileAccess.js";
import {
getRepoInfo,
hasRepo,
existsInRepo,
downloadAndExtractRepo,
downloadAndExtractExample,
} from "./utils/example.js";
import { isFolderEmpty } from "./utils/isFolderEmpty.js";
import { existsSync, writeFileSync } from "fs";
import { install } from "./utils/install.js";
import os from "os";

interface YogaProps {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you please add some comments explaining the flags here?

appPath: string;
packageManager: any;
Copy link
Owner

Choose a reason for hiding this comment

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

should be typed?

Copy link
Owner

Choose a reason for hiding this comment

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

maybe as InstallArgs['packageManager'] ?

template?: string;
templatePath: string;
Copy link
Owner

Choose a reason for hiding this comment

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

I guess we don't need two flags? what's the reason for having two?

typescript: string;
Copy link
Owner

Choose a reason for hiding this comment

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

I think we might not need --typescript flags, as this is already part of the examples setting?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, will remove that.

}

export class DownloadError extends Error {}

export const createYogaApp = async ({
appPath,
packageManager,
template,
templatePath,
typescript,
}: YogaProps) => {
const isTypescript = typescript ? "typescript" : "default";
const example = template;
let repoInfo;

if (example) {
let repoUrl: URL | undefined;

try {
repoUrl = new URL(example);
} catch (error: any) {
if (error.code !== "ERR_INVALID_URL") {
console.error(error);
process.exit(1);
}
}

if (repoUrl) {
if (repoUrl.origin !== "https://github.com") {
console.error(
`Invalid URL: ${chalk.red(
`"${example}"`
)}. Only GitHub repositories are supported. Please use a GitHub URL and try again.`
);
process.exit(1);
}

repoInfo = await getRepoInfo(repoUrl, templatePath);

if (!repoInfo) {
console.error(
`Found invalid GitHub URL: ${chalk.red(
`"${example}"`
)}. Please fix the URL and try again.`
);
process.exit(1);
}

const found = await hasRepo(repoInfo);

if (!found) {
console.error(
`Could not locate the repository for ${chalk.red(
`"${example}"`
)}. Please check that the repository exists and try again.`
);
process.exit(1);
}
} else if (example !== "test") {
const found = await existsInRepo(example);

if (!found) {
console.error(
`Could not locate an example named ${chalk.red(
`"${example}"`
)}. It could be due to the following:\n`,
`1. Your spelling of example ${chalk.red(
`"${example}"`
)} might be incorrect.\n`,
`2. You might not be connected to the internet or you are behind a proxy.`
);
process.exit(1);
}
}
}
const root = path.resolve(appPath);

if (!(await isWriteAccess(path.dirname(root)))) {
console.log(
chalk.red.bold(
"❌ Path provided is not writable, please check the access or change the Path"
)
);
process.exit(1);
}
const appName = path.basename(root);

if (!existsSync(appName)) {
await mkdir(appName);
}

if (!isFolderEmpty(root, appName)) {
process.exit(1);
}

console.log(
chalk.bgGreen.white.italic(
`Creating your Yoga Application in ${chalk.cyan(root)}`
)
);
console.log();

process.chdir(root);

const packageJsonPath = path.join(root, "package.json");
let hasPackageJson = false;

if (example) {
try {
if (repoInfo) {
const repoInfo2 = repoInfo;
console.log(
`Downloading files from repo ${chalk.cyan(
example
)}. This might take a moment.`
);
console.log();
await retry(() => downloadAndExtractRepo(root, repoInfo2), {
retries: 3,
});
} else {
console.log(
`Downloading files form examples of ${chalk.cyan(
example
)} yoga Project. This might take a few moments.`
);
console.log();
await retry(() => downloadAndExtractExample(root, example), {
retries: 3,
});
}
} catch (reason) {
function isErrorLike(err: unknown): err is { message: string } {
return (
typeof err === "object" &&
err !== null &&
typeof (err as { message?: unknown }).message === "string"
);
}
throw new DownloadError(
isErrorLike(reason) ? reason.message : reason + ""
);
}

const gitignore = `
/node_modules
/.pnp
.pnp.js

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local
`;

const ignorePath = path.join(root, ".gitignore");
if (!existsSync(ignorePath)) {
writeFileSync(
path.join(root, ".gitignore"),
JSON.stringify(gitignore, null, 2) + os.EOL
);
}

hasPackageJson = existsSync(packageJsonPath);
if (hasPackageJson) {
console.log("Installing packages. This might take a couple of minutes.");
console.log();

await install(root, null, { packageManager, isOnline: true });
console.log();
} else {
console.error(
"There is no package.json file in the example you've download, skipping Package Installation."
);
}
}

console.log(chalk.cyanBright("Bye, bye!✌🏻"));
};
121 changes: 121 additions & 0 deletions packages/create-yoga/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env node
import { Command } from "commander";
// import packageJson from "./package.json";
import chalk from "chalk";
import prompts from "prompts";
import { validateNpmName } from "./utils/validatePkgName.js";
import path from "path";
import { getPackageManager } from "./utils/getPackageManager.js";
import { createYogaApp } from "./create-yoga.js";

const packageJson = { name: "", version: "1.0" };

let projectOutputPath = "";

const program = new Command(packageJson.name)
.version(packageJson.version)
.configureOutput({
writeErr: (error: string) => console.log(chalk.bgRed.red.bold(error)),
})
.argument("<project-directory>")
.usage("<project-directory> [options]")
.action((name: string) => {
if (!name) {
console.log("Error");
}
projectOutputPath = name;
})
.option("--ts, --typescript", "Create Project using Typescript")
.option(
"--use-npm ",
`

Use NPM instaed of Yarn`
)
.option(
"--use-pnpm",
`

Use PNPM instead of Yarn/NPM`
)
.option(
"-t, --template [name]|[github-url]",
`An example to bootstrap the app with. You can use an example name
from the official GraphQL-yoga repo or a GitHub URL. The URL can use
any branch and/or subdirectory
`
)
.option(
"-p,--path <path-to-example>",
`

In a rare case, your GitHub URL might contain a branch name with
a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar).
In this case, you must specify the path to the example separately:
--path foo/bar
`
)
.allowUnknownOption()
.parse(process.argv);

async function start() {
if (!projectOutputPath || projectOutputPath.length < 1) {
const response = await prompts({
type: "text",
name: "path",
message: "Please enter your project folder name",
validate: (val: string) => val.length > 1,
});

if (!response.path) {
console.log(chalk.bgRed.red.bold("Please enter Project path"));
process.exit(1);
}
projectOutputPath = response?.path.trim();
}
const resolvedProjectPath = path.resolve(projectOutputPath);
const projectName = path.basename(resolvedProjectPath);

const { valid, problems } = validateNpmName(projectName);
// console.log(valid, problems);

if (!valid) {
console.log(
chalk.red.bold(`\n❌ Couldn't able to create Project because: \n\n`)
);

problems?.forEach((p) => console.log(chalk.red.bold(` ${p}\n`)));
process.exit(1);
}

const template = program.getOptionValue("template");
if (typeof template === "boolean" || template === true) {
console.log(
chalk.cyan.italic(
`[EXIT]: Please provide the template name or URL or remove the example options`
)
);
process.exit(1);
}
const useNpm = program.getOptionValue("UseNpm");
const usePnpm = program.getOptionValue("usePnpm");
const packageManager: any = getPackageManager(useNpm, usePnpm);

const project = typeof template === "string" && template.trim();

const typescript = program.getOptionValue("typescript");
const templatePath = program.getOptionValue("path");
try {
await createYogaApp({
appPath: resolvedProjectPath,
packageManager,
template: project || undefined,
templatePath,
typescript,
});
} catch (error) {
console.error(error);
}
}

start();
50 changes: 50 additions & 0 deletions packages/create-yoga/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "create-yoga-app",
"description": "A starter kit Project for creating simple GraphQL Yoga Projects",
"version": "1.0.0",
"exports": "./index.js",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "ncc build ./index.ts -o bin",
"test": "npm run build && yarn link create-yoga-app && yarn create yoga-app ./example -t aws-lambda-bundle",
"prerelease": "rimraf ./dist/",
"release": "ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
"publish": "echo Hi"
},
"repository": {
"url": "https://github.com/dotansimha/graphql-yoga",
"type": "git"
},
"bin": {
"create-yoga-app": "bin/index.js"
},
"keywords": [
"yoga",
"starter-kit",
"graphql",
"graphql-yoga",
"template"
],
"license": "MIT",
"engines": {
"node": ">=14"
},
"devDependencies": {
Copy link
Owner

Choose a reason for hiding this comment

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

We are usually pinning all devDepenencies

"@types/async-retry": "^1.4.4",
"@types/node": "^18.0.3",
"@types/prompts": "^2.0.14",
"@types/tar": "^6.1.1",
"@types/validate-npm-package-name": "^4.0.0",
"async-retry": "^1.3.3",
"chalk": "4.1.2",
"commander": "^9.3.0",
"execa": "^6.1.0",
"got": "^12.1.0",
"prompts": "^2.4.2",
"tar": "^6.1.11",
"typescript": "^4.7.4",
"validate-npm-package-name": "^4.0.0"
}
}
Loading