Skip to content

Commit cfe8f93

Browse files
feat(create-abell): migrate create-abell to monorepo and TS (#136)
* feat: initiate basic create-abell code * feat: add install dependecies step * feat: add deleteDir step * docs: elaborate comment * feat: add default template * feat: add logs * fix: missing projectname error * fix error format * 0.0.15
1 parent 387cf15 commit cfe8f93

File tree

22 files changed

+1254
-0
lines changed

22 files changed

+1254
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Logs
2+
.DS_Store
23
logs
34
*.log
45
npm-debug.log*

packages/create-abell/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
scaffold-dir/

packages/create-abell/.npmignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
dist
3+
scaffold-dir
4+
src
5+
templates/**/dist/
6+
templates/**/node_modules/
7+
templates/**/yarn.lock
8+
scripts
9+
tsconfig.json

packages/create-abell/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "create-abell",
3+
"version": "0.0.15",
4+
"description": "Boilerplate for abell. npx create-abell my-blog",
5+
"main": "dist/create.js",
6+
"types": "dist/create.d.ts",
7+
"bin": {
8+
"create-abell": "dist/bin.js"
9+
},
10+
"scripts": {
11+
"build": "tsc && node scripts/post-build.js",
12+
"dev": "nodemon --exec \"yarn build\" --watch src --watch templates -e js,ts,abell,css",
13+
"clean-scaffolds": "node scripts/clean-scaffolds.js",
14+
"scaffold": "npm run clean-scaffolds && cd scaffold-dir && node ../dist/bin.js",
15+
"prepublishOnly": "yarn build"
16+
},
17+
"repository": {
18+
"type": "git",
19+
"url": "git+https://github.com/abelljs/abell.git"
20+
},
21+
"keywords": [
22+
"abell"
23+
],
24+
"author": "saurabhdaware",
25+
"license": "MIT",
26+
"bugs": {
27+
"url": "https://github.com/abelljs/abell/issues"
28+
},
29+
"homepage": "https://github.com/abelljs/abell#readme",
30+
"dependencies": {
31+
"commander": "^9.2.0",
32+
"prompts": "^2.4.2"
33+
},
34+
"devDependencies": {
35+
"@types/prompts": "^2.0.14"
36+
}
37+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const { deleteDir } = require('../dist/utils');
4+
5+
function clean() {
6+
const scaffoldDir = path.join(__dirname, '..', 'scaffold-dir');
7+
deleteDir(scaffoldDir);
8+
fs.mkdirSync(scaffoldDir);
9+
}
10+
11+
clean();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const DIST = path.join(__dirname, '..', 'dist');
5+
6+
try {
7+
const fd = fs.openSync(path.join(DIST, 'bin.js'), 'r');
8+
fs.fchmodSync(fd, 511);
9+
console.log('> Changed bin.js file persmission to executable');
10+
} catch (error) {
11+
console.log(error);
12+
}

packages/create-abell/src/bin.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env node
2+
import { createCommand } from 'commander';
3+
import create, { CreateAbellOptions } from './create';
4+
5+
const program = createCommand();
6+
/**
7+
* npx create-abell [projectName] --template <template> --installer <installer>
8+
*/
9+
program
10+
.option('-t|--template <template>', 'Specify template for abell app')
11+
.option(
12+
'-i|--installer <installer>',
13+
'Specify package installer. npm or yarn.'
14+
)
15+
.arguments('[projectName]')
16+
.action((projectName: string | undefined, options: CreateAbellOptions) =>
17+
create(projectName, {
18+
template: options.template,
19+
installer: options.installer
20+
})
21+
);
22+
23+
// eslint-disable-next-line @typescript-eslint/no-var-requires
24+
program.version(require('../package.json').version, '-v|--version');
25+
program.parse(process.argv);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
getInstallCommand,
3+
getProjectInfo,
4+
getTemplate,
5+
scaffoldTemplate,
6+
setNameInPackageJSON
7+
} from './steps';
8+
import { deleteDir, log, relative, run } from './utils';
9+
10+
export type CreateAbellOptions = {
11+
installer?: 'npm' | 'yarn';
12+
template?: string;
13+
};
14+
15+
async function create(
16+
projectNameArg: string | undefined,
17+
options: CreateAbellOptions
18+
): Promise<void> {
19+
// 1. Get all the required project information
20+
const { projectDisplayName, projectPath } = await getProjectInfo(
21+
projectNameArg
22+
);
23+
const relProjectPath = relative(projectPath);
24+
const template = getTemplate(options.template);
25+
const installCommand = await getInstallCommand(options.installer);
26+
log.info(`Scaffolding \`${relProjectPath}\` using \`${template}\` template`);
27+
28+
// 2. Scaffold Project
29+
await scaffoldTemplate({
30+
projectPath,
31+
template
32+
});
33+
34+
log.info(`Running \`${installCommand}\``);
35+
// 3. Install Dependencies
36+
try {
37+
await run(installCommand, {
38+
cwd: projectPath
39+
});
40+
} catch (err) {
41+
log.failure(`Could not install dependencies. Skipping ${installCommand}`);
42+
}
43+
44+
// 4. Set name in project's package.json
45+
setNameInPackageJSON(`${projectPath}/package.json`, projectDisplayName);
46+
47+
// 5. Delete `.git` (For projects scaffolded from github)
48+
deleteDir(`${projectPath}/.git`);
49+
50+
// 6. Log Success @todo
51+
log.success(`${projectDisplayName} scaffolded successfully`);
52+
const runCommand = installCommand === 'yarn' ? 'yarn dev' : 'npm run dev';
53+
log.info(
54+
`cd ${relProjectPath} and run \`${runCommand}\` to run the dev-server`
55+
);
56+
}
57+
58+
export default create;

packages/create-abell/src/steps.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import prompts from 'prompts';
4+
import { colors, copyFolderSync, log, normalizePath, run } from './utils';
5+
6+
/**
7+
* Prompts user for projectName if not defined, returns the information required related to project
8+
*/
9+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
10+
export const getProjectInfo = async (projectNameArg: string | undefined) => {
11+
let projectName = '';
12+
if (!projectNameArg) {
13+
projectName = (
14+
await prompts({
15+
type: 'text',
16+
message: 'Enter Name of your project',
17+
name: 'projectName',
18+
initial: 'hello-abell'
19+
})
20+
).projectName;
21+
} else {
22+
projectName = projectNameArg;
23+
}
24+
25+
if (!projectName) {
26+
throw new Error(log.failure('Project name is required', false));
27+
}
28+
29+
const projectSlugName = projectName.toLowerCase().replace(/ |_/g, '-');
30+
const projectPath = path.join(process.cwd(), projectSlugName);
31+
const projectDisplayName = path.basename(projectPath);
32+
33+
if (fs.existsSync(projectPath)) {
34+
// oops. Can be an issue
35+
if (fs.readdirSync(projectPath).length !== 0) {
36+
// Not an empty directory so break!
37+
console.error(
38+
`${colors.red(
39+
'>> '
40+
)} The directory already exists and is not an empty directory`
41+
);
42+
process.exit(0);
43+
}
44+
}
45+
46+
return { projectDisplayName, projectPath };
47+
};
48+
49+
/**
50+
* Prompts user to choose package installer if not defined
51+
*/
52+
export const getInstallCommand = async (
53+
installerVal: 'npm' | 'yarn' | undefined
54+
): Promise<'npm install' | 'yarn'> => {
55+
if (!installerVal) {
56+
// if installer flag is undefined, ask user.
57+
const answers = await prompts({
58+
type: 'select',
59+
message: 'Select Installer',
60+
name: 'installer',
61+
choices: [
62+
{
63+
title: 'npm',
64+
value: 'npm install'
65+
},
66+
{
67+
title: 'yarn',
68+
value: 'yarn'
69+
}
70+
]
71+
});
72+
73+
installerVal = answers.installer;
74+
}
75+
76+
if (installerVal === 'yarn') {
77+
return 'yarn';
78+
} else {
79+
return 'npm install';
80+
}
81+
};
82+
83+
/**
84+
* Some validations on top of template names
85+
*/
86+
export const getTemplate = (templateVal: string | undefined): string => {
87+
// return default when value is not defined
88+
if (!templateVal) return 'default';
89+
90+
if (templateVal === 'default' || templateVal === 'minimal') {
91+
// 'default' and 'minimal' are valid templates. Return them as it is
92+
return templateVal;
93+
}
94+
95+
// when `--template abelljs/abell-starter-portfolio`
96+
if (!templateVal.startsWith('https://github.com/')) {
97+
// If template value is `abelljs/abell-starter-portfolio`, add https://github.com before it.
98+
return 'https://github.com/' + templateVal;
99+
}
100+
101+
// when `--template https://github.com/abelljs/abell-starter-portfolio`
102+
return templateVal;
103+
};
104+
105+
export const scaffoldTemplate = async ({
106+
projectPath,
107+
template
108+
}: {
109+
projectPath: string;
110+
template: string;
111+
}): Promise<void> => {
112+
if (template === 'default' || template === 'minimal') {
113+
// copy default template from templates directory
114+
const templatesDir = path.join(__dirname, '..', 'templates');
115+
const templatePath = path.join(templatesDir, template);
116+
copyFolderSync(templatePath, projectPath, [
117+
path.join(templatePath, 'node_modules'),
118+
path.join(templatePath, 'yarn.lock'),
119+
path.join(templatePath, 'dist')
120+
]);
121+
} else {
122+
// Execute git clone
123+
try {
124+
const errorCode = await run(`git clone ${template} ${projectPath}`);
125+
if (errorCode === 1) {
126+
throw new Error(log.failure('Git clone failed', false));
127+
}
128+
} catch (err) {
129+
throw err;
130+
}
131+
}
132+
};
133+
134+
export const setNameInPackageJSON = (
135+
packagePath: string,
136+
appName: string
137+
): void => {
138+
try {
139+
// eslint-disable-next-line @typescript-eslint/no-var-requires
140+
const packageJSON = require(normalizePath(packagePath));
141+
packageJSON.name = appName;
142+
fs.writeFileSync(packagePath, JSON.stringify(packageJSON, null, 2));
143+
} catch (err) {
144+
// Do nothing. Skip the step if error.
145+
}
146+
};

0 commit comments

Comments
 (0)