Skip to content

Commit 3ddfa9d

Browse files
muxiangqiuvangie
authored andcommitted
add fun init (#96)
Author: muxiangqiu <1158025591@qq.com>
1 parent 800392d commit 3ddfa9d

42 files changed

Lines changed: 1479 additions & 28 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.DS_Store

6 KB
Binary file not shown.

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ examples/local/java8/target/*
8080

8181
output
8282
.oss_cfg
83+
8384
*.pyc
8485
*.iml
85-
*.class
86+
*.class
87+
.DS_Store

bin/fun-init.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env node
2+
3+
/* eslint-disable quotes */
4+
5+
'use strict';
6+
7+
const program = require('commander');
8+
9+
const examples =
10+
`
11+
Examples:
12+
13+
$ fun init
14+
$ fun init helloworld-nodejs8
15+
$ fun init foo/bar
16+
$ fun init gh:foo/bar
17+
$ fun init gl:foo/bar
18+
$ fun init bb:foo/bar
19+
$ fun init github:foo/bar
20+
$ fun init gitlab:foo/bar
21+
$ fun init bitbucket:foo/bar
22+
$ fun init git+ssh://git@github.com/foo/bar.git
23+
$ fun init hg+ssh://hg@bitbucket.org/bar/foo
24+
$ fun init git@github.com:foo/bar.git
25+
$ fun init https://github.com/foo/bar.git
26+
$ fun init /path/foo/bar
27+
$ fun init -n fun-app -V foo=bar /path/foo/bar
28+
`;
29+
30+
const parseVars = (val, vars) => {
31+
/*
32+
* Key-value pairs, separated by equal signs
33+
* keys can only contain letters, numbers, and underscores
34+
* values can be any character
35+
*/
36+
const group = val.match(/(^[a-zA-Z_][a-zA-Z\d_]*)=(.*)/);
37+
vars = vars || {};
38+
if (group) {
39+
vars[group[1]] = group[2];
40+
}
41+
return vars;
42+
};
43+
44+
program
45+
.name('fun init')
46+
.usage('[options] [location]')
47+
.description('Initializes a new fun project.')
48+
.option('-o, --output-dir [path]', 'where to output the initialized app into', '.')
49+
.option('-n, --name [name]', 'name of your project to be generated as a folder', 'fun-app')
50+
.option('--no-input [noInput]', 'disable prompting and accept default values defined template config')
51+
.option('-V, --var [vars]', 'template variable', parseVars)
52+
.on('--help', () => {
53+
console.log(examples);
54+
})
55+
.parse(process.argv);
56+
57+
const context = {
58+
name: program.name,
59+
outputDir: program.outputDir,
60+
input: program.input,
61+
vars: program.var || {}
62+
};
63+
64+
if (program.args.length > 0) {
65+
context.location = program.args[0];
66+
}
67+
68+
require('../lib/commands/init')(context).catch(require('../lib/exception-handler'));

bin/fun.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ program
2020
// The commander will try to search the executables in the directory of the entry script
2121
// (like ./examples/pm) with the name program-command.
2222
.command('config', 'configure the fun')
23+
.command('init', 'initialize a new fun project')
2324
.command('build', 'build the dependencies')
2425
.command('local', 'run your serverless application locally')
2526
.command('validate', 'validate a fun template')
@@ -38,4 +39,4 @@ program.on('command:*', (cmds) => {
3839
}
3940
});
4041

41-
program.parse(process.argv);
42+
program.parse(process.argv);

examples/.DS_Store

6 KB
Binary file not shown.

lib/commands/init.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
const { determineRepoDir, getOfficialTemplates } = require('../init/repository');
4+
const { render } = require('../init/renderer');
5+
const { sync } = require('rimraf');
6+
const { buildContext } = require('../init/context');
7+
const { promptForTemplate } = require('../init/prompt');
8+
const debug = require('debug')('fun:init');
9+
10+
function cleanTemplate(repoDir) {
11+
debug('Cleaning Template: %', repoDir);
12+
sync(repoDir);
13+
}
14+
15+
async function init(context) {
16+
debug('location is: %s', context.location);
17+
context.templates = getOfficialTemplates();
18+
if (!context.location) {
19+
context.location = await promptForTemplate(Object.keys(context.templates));
20+
}
21+
const {repoDir, clean} = await determineRepoDir(context);
22+
await buildContext(repoDir, context);
23+
render(context);
24+
if (clean) {
25+
cleanTemplate(repoDir);
26+
}
27+
28+
}
29+
30+
module.exports = init;

lib/init/config.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
'use strict';
3+
4+
const fs = require('fs');
5+
const { isArray } = require('lodash/lang');
6+
const path = require('path');
7+
const { renderContent } = require('./renderer');
8+
const requireFromString = require('require-from-string');
9+
const debug = require('debug')('fun:config');
10+
11+
function getConfig(context) {
12+
let configPath = path.resolve(context.repoDir, 'metadata.json');
13+
let isJSON = true;
14+
15+
if (!fs.existsSync(configPath)) {
16+
configPath = path.resolve(context.repoDir, 'metadata.js');
17+
if (!fs.existsSync(configPath)) {
18+
return {};
19+
}
20+
isJSON = false;
21+
}
22+
23+
debug('configPath is %s', configPath);
24+
const renderedContent = renderContent(fs.readFileSync(configPath, 'utf8'), context);
25+
let config;
26+
if (isJSON) {
27+
try {
28+
config = JSON.parse(renderedContent);
29+
} catch (err) {
30+
throw new Error(`Unable to parse JSON file ${configPath}. Error: ${err}`);
31+
}
32+
} else {
33+
config = requireFromString(renderedContent);
34+
}
35+
if (isArray(config.copyOnlyPaths)) {
36+
config.copyOnlyPaths = config.copyOnlyPaths.join('\n');
37+
}
38+
return config;
39+
}
40+
41+
module.exports = { getConfig };

lib/init/context.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
'use strict';
3+
4+
const { getConfig } = require('./config');
5+
const { makeSurePathExists } = require('./vcs');
6+
const { promptForConfig, promptForExistingPath } = require('./prompt');
7+
const templateSettings = require('lodash/templateSettings');
8+
const { renderContent } = require('./renderer');
9+
const fs = require('fs');
10+
const path = require('path');
11+
12+
const debug = require('debug')('fun:context');
13+
14+
function isTemplated(dirname) {
15+
if (templateSettings.interpolate.test(dirname)) {
16+
return true;
17+
}
18+
return false;
19+
}
20+
21+
function findTemplate(repoDir) {
22+
debug(`Searching ${ repoDir } for project template.`);
23+
24+
const files = fs.readdirSync(repoDir);
25+
let templateDir = '';
26+
files.forEach(file => {
27+
if (isTemplated(file)) {
28+
templateDir = file;
29+
return false;
30+
}
31+
});
32+
if (templateDir) {
33+
return templateDir;
34+
}
35+
throw new Error('Non template input dir.');
36+
}
37+
38+
async function buildContext(repoDir, context) {
39+
context.vars.projectName = context.name;
40+
context.repoDir = repoDir;
41+
42+
const templateDir = findTemplate(repoDir);
43+
context.templateDir = templateDir;
44+
const renderedDir = renderContent(templateDir, context);
45+
const fullTargetDir = path.resolve(context.outputDir, renderedDir);
46+
makeSurePathExists(path.resolve(context.outputDir));
47+
debug(`Generating project to ${ fullTargetDir }...`);
48+
await promptForExistingPath(fullTargetDir, `You've created ${fullTargetDir} before. Is it okay to delete and recreate it?`);
49+
50+
const config = getConfig(context);
51+
context.config = config;
52+
context.vars = Object.assign(config.vars || {}, context.vars);
53+
await promptForConfig(context);
54+
55+
debug(`Context is ${ JSON.stringify(context) }`);
56+
}
57+
58+
module.exports = { buildContext };

lib/init/prompt.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
const inquirer = require('inquirer');
4+
const fs = require('fs');
5+
const { isEmpty, isArray } = require('lodash/lang');
6+
const { sync } = require('rimraf');
7+
const debug = require('debug')('fun:prompt');
8+
9+
inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'));
10+
11+
async function promptForConfig(context) {
12+
let userPrompt = context.config.userPrompt;
13+
14+
if (isEmpty(userPrompt)) {
15+
return;
16+
}
17+
if (!isArray(userPrompt)) {
18+
userPrompt = [userPrompt];
19+
}
20+
const questions = userPrompt.filter(q => !(q.name in context.vars));
21+
if (isEmpty(questions)) {
22+
return;
23+
}
24+
if (context.input) {
25+
debug('Config Need prompt.');
26+
Object.assign(context.vars, await inquirer.prompt(questions));
27+
} else {
28+
debug('Config does not need prompt.');
29+
const defaultVars = {};
30+
questions.forEach(q => {
31+
defaultVars[q.name] = q.default;
32+
});
33+
context.vars = Object.assign(defaultVars, context.vars);
34+
}
35+
36+
}
37+
38+
async function promptForExistingPath(path, message) {
39+
if (!fs.existsSync(path)) {
40+
return;
41+
}
42+
const answers = await inquirer.prompt([{
43+
type: 'confirm',
44+
name: 'okToDelete',
45+
message: message
46+
}]);
47+
if (answers.okToDelete) {
48+
try {
49+
sync(path);
50+
} catch (err) {
51+
throw new Error(`Failed to delete file or folder: ${path}, error is: ${err}`);
52+
}
53+
} else {
54+
process.exit(-1);
55+
}
56+
}
57+
58+
async function promptForTemplate(templates) {
59+
return inquirer.prompt([{
60+
type: 'autocomplete',
61+
name: 'template',
62+
message: 'Select a tempalte to init',
63+
pageSize: 16,
64+
source: async (answersForFar, input) => {
65+
input = input || '';
66+
return templates.filter(t => t.toLowerCase().includes(input.toLowerCase()));
67+
}
68+
}]).then(answers => {
69+
return answers.template;
70+
});
71+
}
72+
73+
74+
75+
module.exports = { promptForConfig, promptForExistingPath, promptForTemplate };

lib/init/renderer.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
3+
const parser = require('git-ignore-parser');
4+
const ignore = require('ignore');
5+
const fs = require('fs');
6+
const path = require('path');
7+
const template = require('lodash/template');
8+
const templateSettings = require('lodash/templateSettings');
9+
const debug = require('debug')('fun:renderer');
10+
const { green } = require('colors');
11+
templateSettings.interpolate = /{{([\s\S]+?)}}/g;
12+
13+
function renderContent(content, context) {
14+
return template(content)(context.vars);
15+
}
16+
17+
function isCopyOnlyPath(file, context) {
18+
const copyOnlyPaths = context.config.copyOnlyPaths;
19+
if (copyOnlyPaths) {
20+
debug(`copyOnlyPath is ${ copyOnlyPaths }`);
21+
const ignoredPaths = parser(copyOnlyPaths);
22+
const ig = ignore().add(ignoredPaths);
23+
const relativePath = path.relative(path.resolve(context.repoDir, context.templateDir), file);
24+
debug(`relativePath is ${ relativePath }`);
25+
return ig.ignores(relativePath);
26+
}
27+
return false;
28+
}
29+
30+
function renderFile(file, context) {
31+
const renderedFile = renderContent(file, context);
32+
const fullSourceFile = path.resolve(context.repoDir, file);
33+
const fullTargetFile= path.resolve(context.outputDir, renderedFile);
34+
debug('Source file: %s, target file: %s', fullSourceFile, fullTargetFile);
35+
console.log(green(`+ ${ fullTargetFile }`));
36+
37+
if (isCopyOnlyPath(fullSourceFile, context)) {
38+
debug('Copy %s to %s', fullSourceFile, fullTargetFile);
39+
fs.createReadStream(fullSourceFile).pipe(fs.createWriteStream(fullTargetFile));
40+
return;
41+
}
42+
43+
const content = fs.readFileSync(fullSourceFile, 'utf8');
44+
const renderedContent = renderContent(content, context);
45+
46+
fs.writeFileSync(fullTargetFile, renderedContent);
47+
}
48+
49+
function renderDir(dir, context) {
50+
const renderedDir = renderContent(dir, context);
51+
const fullSourceDir = path.resolve(context.repoDir, dir);
52+
const fullTargetDir = path.resolve(context.outputDir, renderedDir);
53+
54+
debug('Source Dir: %s, target dir: %s', fullSourceDir, fullTargetDir);
55+
console.log(green(`+ ${ fullTargetDir }`));
56+
fs.mkdirSync(fullTargetDir);
57+
const files = fs.readdirSync(fullSourceDir);
58+
files.forEach(file => {
59+
const targetFile = path.join(dir, file);
60+
const fullTargetFile = path.resolve(fullSourceDir, file);
61+
var stat = fs.statSync(fullTargetFile);
62+
if (stat && stat.isDirectory()) {
63+
renderDir(targetFile, context);
64+
} else {
65+
renderFile(targetFile, context);
66+
}
67+
});
68+
}
69+
70+
function render(context) {
71+
console.log('Start rendering template...');
72+
renderDir(context.templateDir, context);
73+
console.log('finish rendering template.');
74+
}
75+
76+
module.exports = { render, renderContent };

0 commit comments

Comments
 (0)