Skip to content

Commit

Permalink
Feat/create app (#334)
Browse files Browse the repository at this point in the history
* feat: Template Init

* feat: Template Init

* feat: Create App Commander
  • Loading branch information
rrr523 authored Sep 15, 2023
1 parent 0c138c7 commit 03be217
Show file tree
Hide file tree
Showing 20 changed files with 1,400 additions and 129 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-pillows-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bnb-chain/create-gnfd-app': patch
---

feat: 🎉 Create App Commander
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ You can try out some examples directly in your browser through Stackblitz:
### Running Examples Locally

Clone the project and install dependencies:

```bash
> git clone [email protected]:bnb-chain/greenfield-js-sdk.git
> cd greenfield-js-sdk
> pnpm install
```

and build package:
Build package:
```bash
> pnpm run -r build
> pnpm run -F "./packages/**" -r build
```

and then copy env template file:
copy env template file:
```bash
> cp .env.simple .env
```
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-strip": "^3.0.2",
"@rollup/plugin-typescript": "^8.5.0",
"rollup": "^2.79.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-polyfill-node": "^0.10.2",
"rollup-plugin-terser": "^7.0.2",
"@types/chai": "^4.3.5",
"@types/mocha": "^9.1.1",
"@types/node": "^18.16.18",
Expand All @@ -45,4 +49,4 @@
"prettier": "^2.8.8",
"rimraf": "^3.0.2"
}
}
}
4 changes: 0 additions & 4 deletions packages/chain-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@
"@types/xml2js": "^0.4.11",
"jest": "^29.5.0",
"mime": "^3.0.0",
"rollup": "^2.79.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-polyfill-node": "^0.10.2",
"rollup-plugin-terser": "^7.0.2",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
Expand Down
7 changes: 7 additions & 0 deletions packages/create-gnfd-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Create Greemfielf App

`create-gnfd-app` allows you to create a new Greenfield app within seconds.

```bash
> npx @bnb-chain/create-gnfd-app
```
58 changes: 58 additions & 0 deletions packages/create-gnfd-app/createApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import download from 'download-git-repo';
import path from 'path';
import { green } from 'picocolors';
import { PackageManager } from './helpers/get-pkg-manager';
import { installTemplate, TemplateType } from './helpers/install-template';
import { isFolderEmpty } from './helpers/is-folder-empty';
import { isWriteable, makeDir } from './helpers/is-writeable';
import { failSpinner, startSpinner, succeedSpiner } from './helpers/spinner';
import { TEMPLATES_MAP } from './templates';

export async function createApp({
appPath,
packageManager,
template,
}: {
appPath: string;
packageManager: PackageManager;
template: TemplateType;
}) {
const root = path.resolve(appPath);
if (!(await isWriteable(path.dirname(root)))) {
/* eslint-disable-next-line no-console */
console.error(
`The application path is not writable, please check folder permissions and try again.
It is likely you do not have write permissions for this folder.
`,
);
process.exit(1);
}

const appName = path.basename(root);
await makeDir(root);
if (!isFolderEmpty(root, appName)) {
process.exit(1);
}

/* eslint-disable-next-line no-console */
console.log(`Creating a new Greenfield app in ${green(root)}.`);
process.chdir(root);

startSpinner('downloading template...');
download(TEMPLATES_MAP[template], '.', { clone: false }, (err) => {
if (err) {
failSpinner(err.message);
return;
}

succeedSpiner(`download template - ${template} success`);

return installTemplate({
appName,
root,
packageManager,
});
}).then(() => {
// ...
});
}
Binary file added packages/create-gnfd-app/example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions packages/create-gnfd-app/helpers/get-pkg-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type PackageManager = 'npm' | 'pnpm' | 'yarn';

export function getPkgManager(): PackageManager {
const userAgent = process.env.npm_config_user_agent || '';

if (userAgent.startsWith('yarn')) {
return 'yarn';
}

if (userAgent.startsWith('pnpm')) {
return 'pnpm';
}

return 'npm';
}
3 changes: 3 additions & 0 deletions packages/create-gnfd-app/helpers/get-template-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const getTemplateUrl = (template: string) => {
return `https://github.com:rrr523/greenfield-${template}-template#master`;
};
58 changes: 58 additions & 0 deletions packages/create-gnfd-app/helpers/install-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import spawn from 'cross-spawn';
import fs from 'fs';
import { removeSync } from 'fs-extra';
import handlebars from 'handlebars';
import { cyan } from 'picocolors';
import { PackageManager } from './get-pkg-manager';
import { failSpinner, startSpinner, succeedSpiner } from './spinner';

export const SRC_DIR_NAMES = ['app', 'pages', 'styles'];

export type TemplateType = 'nextjs' | 'cra';
export type TemplateMode = 'js' | 'ts';

export const installTemplate = async ({ appName, root, packageManager }: InstallTemplateArgs) => {
/**
* Create a package.json for the new project and write it to disk.
*/
const packageJsonPath = `${root}/package.json`;
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
const packageJsonResult = handlebars.compile(packageJsonContent)({
name: appName,
});
fs.writeFileSync(packageJsonPath, packageJsonResult);

/**
* delete package-lock.json
*/
removeSync(`${root}/package-lock.json`);

/**
* install dependencies
*/
startSpinner(cyan(`Installing dependencies with ${packageManager}...`));
const child = spawn(packageManager, ['install'], {
// stdio: 'inherit',
// env: {
// ...process.env,
// ADBLOCK: '1',
// NODE_ENV: 'development',
// DISABLE_OPENCOLLECTIVE: '1',
// },
});

child.on('close', (code) => {
if (code !== 0) {
failSpinner('Failed to install dependencies.');
return;
}

succeedSpiner('Install dependencies successfully.');
});
};

export interface InstallTemplateArgs {
appName: string;
root: string;
packageManager: PackageManager;
}
58 changes: 58 additions & 0 deletions packages/create-gnfd-app/helpers/is-folder-empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* eslint-disable no-console */
import { green, blue } from 'picocolors';
import fs from 'fs';
import path from 'path';

export function isFolderEmpty(root: string, name: string): boolean {
const validFiles = [
'.DS_Store',
'.git',
'.gitattributes',
'.gitignore',
'.gitlab-ci.yml',
'.hg',
'.hgcheck',
'.hgignore',
'.idea',
'.npmignore',
'.travis.yml',
'LICENSE',
'Thumbs.db',
'docs',
'mkdocs.yml',
'npm-debug.log',
'yarn-debug.log',
'yarn-error.log',
'yarnrc.yml',
'.yarn',
];

const conflicts = fs
.readdirSync(root)
.filter((file) => !validFiles.includes(file))
// Support IntelliJ IDEA-based editors
.filter((file) => !/\.iml$/.test(file));

if (conflicts.length > 0) {
console.log(`The directory ${green(name)} contains files that could conflict:`);
console.log();
for (const file of conflicts) {
try {
const stats = fs.lstatSync(path.join(root, file));
if (stats.isDirectory()) {
console.log(` ${blue(file)}/`);
} else {
console.log(` ${file}`);
}
} catch {
console.log(` ${file}`);
}
}
console.log();
console.log('Either try using a new directory name, or remove the files listed above.');
console.log();
return false;
}

return true;
}
14 changes: 14 additions & 0 deletions packages/create-gnfd-app/helpers/is-writeable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import fs from 'fs';

export async function isWriteable(directory: string): Promise<boolean> {
try {
await fs.promises.access(directory, (fs.constants || fs).W_OK);
return true;
} catch (err) {
return false;
}
}

export function makeDir(root: string, options = { recursive: true }): Promise<string | undefined> {
return fs.promises.mkdir(root, options);
}
20 changes: 20 additions & 0 deletions packages/create-gnfd-app/helpers/spinner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ora from 'ora';
import { red } from 'picocolors';

const spinner = ora();

export const startSpinner = (text?: string) => {
const msg = `${text}...\n`;
spinner.start(msg);
};

export const succeedSpiner = (text?: string) => {
spinner.stopAndPersist({
symbol: '🎉',
text: `${text}\n`,
});
};

export const failSpinner = (text?: string) => {
spinner.fail(red(text));
};
94 changes: 94 additions & 0 deletions packages/create-gnfd-app/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import prompts from '@inquirer/prompts';
import Commander from 'commander';
import fs, { readFileSync } from 'fs';
import path, { resolve } from 'path';
import { cyan, green } from 'picocolors';
import validateNpmName from 'validate-npm-package-name';
import { createApp } from './createApp';
import { getPkgManager, PackageManager } from './helpers/get-pkg-manager';
import { TemplateType } from './helpers/install-template';
import { isFolderEmpty } from './helpers/is-folder-empty';

const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
let projectPath = '';
// const projectName = 'xxx';

const program = new Commander.Command(packageJson.name);
program
.version(packageJson.version)
.argument('[project-directory]')
.usage(`${green('[project-directory]')} [options]`)
.action((str) => {
projectPath = str;
})
.allowUnknownOption()
.parse(process.argv);

async function runInitPrompts(): Promise<void> {
if (!projectPath) {
projectPath = await prompts.input({
message: 'What is your project named?',
validate: (val) => {
const { validForNewPackages } = validateNpmName(val);
if (!validForNewPackages) {
return 'Invalid NPM name';
}
return true;
},
});

projectPath = projectPath.trim();
}

if (!projectPath) {
/* eslint-disable-next-line no-console */
console.log(
'\nPlease specify the project directory:\n' +
` ${cyan(program.name())} ${green('<project-directory>')}\n` +
'For example:\n' +
` ${cyan(program.name())} ${green('my-next-app')}\n\n` +
`Run ${cyan(`${program.name()} --help`)} to see all options.`,
);
process.exit(1);
}

const template: TemplateType = await prompts.select({
message: 'select a template?',
choices: [
{ name: 'nextjs', value: 'nextjs' },
{ name: 'create-react-app', value: 'cra' },
],
});

const packageManager: PackageManager = await prompts.select({
message: 'select a package manager?',
choices: [
{ name: 'npm', value: 'npm' },
{ name: 'yarn', value: 'yarn' },
{ name: 'pnpm', value: 'pnpm' },
],
});

const resolvedProjectPath = path.resolve(projectPath);
const root = path.resolve(resolvedProjectPath);
const appName = path.basename(root);
const folderExists = fs.existsSync(root);

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

try {
await createApp({
appPath: resolvedProjectPath,
packageManager,
template,
});
} catch (reason) {
// .
}
}

runInitPrompts().catch(() => {
// ignore error
});
Loading

0 comments on commit 03be217

Please sign in to comment.