Skip to content

Commit

Permalink
add fun init (#96)
Browse files Browse the repository at this point in the history
Author:    muxiangqiu <[email protected]>
  • Loading branch information
muxiangqiu authored and vangie committed Dec 2, 2018
1 parent 800392d commit 3ddfa9d
Show file tree
Hide file tree
Showing 42 changed files with 1,479 additions and 28 deletions.
Binary file added .DS_Store
Binary file not shown.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ examples/local/java8/target/*

output
.oss_cfg

*.pyc
*.iml
*.class
*.class
.DS_Store
68 changes: 68 additions & 0 deletions bin/fun-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env node

/* eslint-disable quotes */

'use strict';

const program = require('commander');

const examples =
`
Examples:
$ fun init
$ fun init helloworld-nodejs8
$ fun init foo/bar
$ fun init gh:foo/bar
$ fun init gl:foo/bar
$ fun init bb:foo/bar
$ fun init github:foo/bar
$ fun init gitlab:foo/bar
$ fun init bitbucket:foo/bar
$ fun init git+ssh://[email protected]/foo/bar.git
$ fun init hg+ssh://[email protected]/bar/foo
$ fun init [email protected]:foo/bar.git
$ fun init https://github.com/foo/bar.git
$ fun init /path/foo/bar
$ fun init -n fun-app -V foo=bar /path/foo/bar
`;

const parseVars = (val, vars) => {
/*
* Key-value pairs, separated by equal signs
* keys can only contain letters, numbers, and underscores
* values can be any character
*/
const group = val.match(/(^[a-zA-Z_][a-zA-Z\d_]*)=(.*)/);
vars = vars || {};
if (group) {
vars[group[1]] = group[2];
}
return vars;
};

program
.name('fun init')
.usage('[options] [location]')
.description('Initializes a new fun project.')
.option('-o, --output-dir [path]', 'where to output the initialized app into', '.')
.option('-n, --name [name]', 'name of your project to be generated as a folder', 'fun-app')
.option('--no-input [noInput]', 'disable prompting and accept default values defined template config')
.option('-V, --var [vars]', 'template variable', parseVars)
.on('--help', () => {
console.log(examples);
})
.parse(process.argv);

const context = {
name: program.name,
outputDir: program.outputDir,
input: program.input,
vars: program.var || {}
};

if (program.args.length > 0) {
context.location = program.args[0];
}

require('../lib/commands/init')(context).catch(require('../lib/exception-handler'));
3 changes: 2 additions & 1 deletion bin/fun.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ program
// The commander will try to search the executables in the directory of the entry script
// (like ./examples/pm) with the name program-command.
.command('config', 'configure the fun')
.command('init', 'initialize a new fun project')
.command('build', 'build the dependencies')
.command('local', 'run your serverless application locally')
.command('validate', 'validate a fun template')
Expand All @@ -38,4 +39,4 @@ program.on('command:*', (cmds) => {
}
});

program.parse(process.argv);
program.parse(process.argv);
Binary file added examples/.DS_Store
Binary file not shown.
30 changes: 30 additions & 0 deletions lib/commands/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

const { determineRepoDir, getOfficialTemplates } = require('../init/repository');
const { render } = require('../init/renderer');
const { sync } = require('rimraf');
const { buildContext } = require('../init/context');
const { promptForTemplate } = require('../init/prompt');
const debug = require('debug')('fun:init');

function cleanTemplate(repoDir) {
debug('Cleaning Template: %', repoDir);
sync(repoDir);
}

async function init(context) {
debug('location is: %s', context.location);
context.templates = getOfficialTemplates();
if (!context.location) {
context.location = await promptForTemplate(Object.keys(context.templates));
}
const {repoDir, clean} = await determineRepoDir(context);
await buildContext(repoDir, context);
render(context);
if (clean) {
cleanTemplate(repoDir);
}

}

module.exports = init;
41 changes: 41 additions & 0 deletions lib/init/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

'use strict';

const fs = require('fs');
const { isArray } = require('lodash/lang');
const path = require('path');
const { renderContent } = require('./renderer');
const requireFromString = require('require-from-string');
const debug = require('debug')('fun:config');

function getConfig(context) {
let configPath = path.resolve(context.repoDir, 'metadata.json');
let isJSON = true;

if (!fs.existsSync(configPath)) {
configPath = path.resolve(context.repoDir, 'metadata.js');
if (!fs.existsSync(configPath)) {
return {};
}
isJSON = false;
}

debug('configPath is %s', configPath);
const renderedContent = renderContent(fs.readFileSync(configPath, 'utf8'), context);
let config;
if (isJSON) {
try {
config = JSON.parse(renderedContent);
} catch (err) {
throw new Error(`Unable to parse JSON file ${configPath}. Error: ${err}`);
}
} else {
config = requireFromString(renderedContent);
}
if (isArray(config.copyOnlyPaths)) {
config.copyOnlyPaths = config.copyOnlyPaths.join('\n');
}
return config;
}

module.exports = { getConfig };
58 changes: 58 additions & 0 deletions lib/init/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

'use strict';

const { getConfig } = require('./config');
const { makeSurePathExists } = require('./vcs');
const { promptForConfig, promptForExistingPath } = require('./prompt');
const templateSettings = require('lodash/templateSettings');
const { renderContent } = require('./renderer');
const fs = require('fs');
const path = require('path');

const debug = require('debug')('fun:context');

function isTemplated(dirname) {
if (templateSettings.interpolate.test(dirname)) {
return true;
}
return false;
}

function findTemplate(repoDir) {
debug(`Searching ${ repoDir } for project template.`);

const files = fs.readdirSync(repoDir);
let templateDir = '';
files.forEach(file => {
if (isTemplated(file)) {
templateDir = file;
return false;
}
});
if (templateDir) {
return templateDir;
}
throw new Error('Non template input dir.');
}

async function buildContext(repoDir, context) {
context.vars.projectName = context.name;
context.repoDir = repoDir;

const templateDir = findTemplate(repoDir);
context.templateDir = templateDir;
const renderedDir = renderContent(templateDir, context);
const fullTargetDir = path.resolve(context.outputDir, renderedDir);
makeSurePathExists(path.resolve(context.outputDir));
debug(`Generating project to ${ fullTargetDir }...`);
await promptForExistingPath(fullTargetDir, `You've created ${fullTargetDir} before. Is it okay to delete and recreate it?`);

const config = getConfig(context);
context.config = config;
context.vars = Object.assign(config.vars || {}, context.vars);
await promptForConfig(context);

debug(`Context is ${ JSON.stringify(context) }`);
}

module.exports = { buildContext };
75 changes: 75 additions & 0 deletions lib/init/prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';

const inquirer = require('inquirer');
const fs = require('fs');
const { isEmpty, isArray } = require('lodash/lang');
const { sync } = require('rimraf');
const debug = require('debug')('fun:prompt');

inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'));

async function promptForConfig(context) {
let userPrompt = context.config.userPrompt;

if (isEmpty(userPrompt)) {
return;
}
if (!isArray(userPrompt)) {
userPrompt = [userPrompt];
}
const questions = userPrompt.filter(q => !(q.name in context.vars));
if (isEmpty(questions)) {
return;
}
if (context.input) {
debug('Config Need prompt.');
Object.assign(context.vars, await inquirer.prompt(questions));
} else {
debug('Config does not need prompt.');
const defaultVars = {};
questions.forEach(q => {
defaultVars[q.name] = q.default;
});
context.vars = Object.assign(defaultVars, context.vars);
}

}

async function promptForExistingPath(path, message) {
if (!fs.existsSync(path)) {
return;
}
const answers = await inquirer.prompt([{
type: 'confirm',
name: 'okToDelete',
message: message
}]);
if (answers.okToDelete) {
try {
sync(path);
} catch (err) {
throw new Error(`Failed to delete file or folder: ${path}, error is: ${err}`);
}
} else {
process.exit(-1);
}
}

async function promptForTemplate(templates) {
return inquirer.prompt([{
type: 'autocomplete',
name: 'template',
message: 'Select a tempalte to init',
pageSize: 16,
source: async (answersForFar, input) => {
input = input || '';
return templates.filter(t => t.toLowerCase().includes(input.toLowerCase()));
}
}]).then(answers => {
return answers.template;
});
}



module.exports = { promptForConfig, promptForExistingPath, promptForTemplate };
76 changes: 76 additions & 0 deletions lib/init/renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict';

const parser = require('git-ignore-parser');
const ignore = require('ignore');
const fs = require('fs');
const path = require('path');
const template = require('lodash/template');
const templateSettings = require('lodash/templateSettings');
const debug = require('debug')('fun:renderer');
const { green } = require('colors');
templateSettings.interpolate = /{{([\s\S]+?)}}/g;

function renderContent(content, context) {
return template(content)(context.vars);
}

function isCopyOnlyPath(file, context) {
const copyOnlyPaths = context.config.copyOnlyPaths;
if (copyOnlyPaths) {
debug(`copyOnlyPath is ${ copyOnlyPaths }`);
const ignoredPaths = parser(copyOnlyPaths);
const ig = ignore().add(ignoredPaths);
const relativePath = path.relative(path.resolve(context.repoDir, context.templateDir), file);
debug(`relativePath is ${ relativePath }`);
return ig.ignores(relativePath);
}
return false;
}

function renderFile(file, context) {
const renderedFile = renderContent(file, context);
const fullSourceFile = path.resolve(context.repoDir, file);
const fullTargetFile= path.resolve(context.outputDir, renderedFile);
debug('Source file: %s, target file: %s', fullSourceFile, fullTargetFile);
console.log(green(`+ ${ fullTargetFile }`));

if (isCopyOnlyPath(fullSourceFile, context)) {
debug('Copy %s to %s', fullSourceFile, fullTargetFile);
fs.createReadStream(fullSourceFile).pipe(fs.createWriteStream(fullTargetFile));
return;
}

const content = fs.readFileSync(fullSourceFile, 'utf8');
const renderedContent = renderContent(content, context);

fs.writeFileSync(fullTargetFile, renderedContent);
}

function renderDir(dir, context) {
const renderedDir = renderContent(dir, context);
const fullSourceDir = path.resolve(context.repoDir, dir);
const fullTargetDir = path.resolve(context.outputDir, renderedDir);

debug('Source Dir: %s, target dir: %s', fullSourceDir, fullTargetDir);
console.log(green(`+ ${ fullTargetDir }`));
fs.mkdirSync(fullTargetDir);
const files = fs.readdirSync(fullSourceDir);
files.forEach(file => {
const targetFile = path.join(dir, file);
const fullTargetFile = path.resolve(fullSourceDir, file);
var stat = fs.statSync(fullTargetFile);
if (stat && stat.isDirectory()) {
renderDir(targetFile, context);
} else {
renderFile(targetFile, context);
}
});
}

function render(context) {
console.log('Start rendering template...');
renderDir(context.templateDir, context);
console.log('finish rendering template.');
}

module.exports = { render, renderContent };
Loading

0 comments on commit 3ddfa9d

Please sign in to comment.