Repository: aliyun/fun
Pull request #146 (2019-01-30 07:21:35)

add igonrePaths
变更如下：
1. 模板配置文件支持 `ignorePaths`, 用来忽略模板项目中某些文件不需要渲染或者拷贝到目标目录
2. 添加了新 Region：`cn-huhehaote`
3. fun init [options] [location] 改为 fun init [options] [template]，并为 template 参数添加了详细的说明 
4. `ignorePaths` 和 `copyOnlyPaths` 参数支持字符串和数组两种类型


Patch:
diff --git a/bin/fun-init.js b/bin/fun-init.js
index eae3c2b..1951d00 100644
--- a/bin/fun-init.js
+++ b/bin/fun-init.js
@@ -43,8 +43,8 @@ const parseVars = (val, vars) => {
 
 program
   .name('fun init')
-  .usage('[options] [location]')
-  .description('Initializes a new fun project.')
+  .usage('[options] [template]')
+  .description('Initialize a new project based on a template. A template can be a folder containing template metadata and boilerplate files, a name of a pre-built template, or a url that resolves to a template. You can find more information about template at https://yq.aliyun.com/articles/674364.')
   .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', '')
   .option('-m, --merge [merge]', 'merge into the template.[yml|yaml] file if it already exist', false)
diff --git a/lib/commands/config.js b/lib/commands/config.js
index 12f587e..94008c6 100644
--- a/lib/commands/config.js
+++ b/lib/commands/config.js
@@ -41,7 +41,7 @@ async function config() {
       name: 'defaultRegion',
       message: 'Default region name',
       choices: ['cn-qingdao', 'cn-beijing', 'cn-zhangjiakou', 
-        'cn-hangzhou', 'cn-shanghai', 'cn-shenzhen' , 
+        'cn-hangzhou', 'cn-shanghai', 'cn-shenzhen', 'cn-huhehaote',
         'cn-hongkong', 'ap-southeast-1', 'ap-southeast-2', 
         'ap-northeast-1', 'us-west-1', 'us-east-1', 
         'eu-central-1', 'ap-south-1'],
diff --git a/lib/init/context.js b/lib/init/context.js
index 2ee6a12..0d3b4e2 100644
--- a/lib/init/context.js
+++ b/lib/init/context.js
@@ -35,6 +35,15 @@ function findTemplate(repoDir) {
   throw new Error('Non template input dir.');
 }
 
+function doBuildPaths(paths) {
+  if (typeof paths === 'string'){
+    return paths;
+  } else if (paths instanceof Array) {
+    return paths.join('\n');
+  }
+  throw Error(`The ${ typeof paths } type not supported.`);
+}
+
 async function buildContext(repoDir, context) {
   if (context.name) {
     context.vars.projectName = context.name;
@@ -59,6 +68,12 @@ async function buildContext(repoDir, context) {
 
   const config = getConfig(context);
   context.config = config;
+  if (context.config.copyOnlyPaths) {
+    context.config.copyOnlyPaths = doBuildPaths(context.config.copyOnlyPaths);
+  }
+  if (context.config.ignorePaths) {
+    context.config.ignorePaths = doBuildPaths(context.config.ignorePaths);
+  }
   context.vars = Object.assign(config.vars || {}, context.vars);
   await promptForConfig(context);
 
diff --git a/lib/init/renderer.js b/lib/init/renderer.js
index efe3cb9..9846228 100644
--- a/lib/init/renderer.js
+++ b/lib/init/renderer.js
@@ -16,19 +16,31 @@ 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);
+function isMatch(file, paths, context) {
+  if (paths) {
+    debug(`Path is ${ paths }`);
+    const ignoredPaths = parser(paths);
     const ig = ignore().add(ignoredPaths);
     const relativePath = path.relative(path.resolve(context.repoDir, context.templateDir), file);
+    if (!relativePath) {
+      return false;
+    }
     debug(`relativePath is ${ relativePath }`);
     return ig.ignores(relativePath);
   }
   return false;
 }
 
+function isCopyOnlyPath(file, context) {
+  const copyOnlyPaths = context.config.copyOnlyPaths;
+  return isMatch(file, copyOnlyPaths, context);
+}
+
+function isIgnorePaths(file, context) {
+  const ignorePaths = context.config.ignorePaths;
+  return isMatch(file, ignorePaths, context);
+}
+
 function needMerge(file, context) {
   return context.merge
     && /template.(yml|yaml)$/.test(file)
@@ -64,6 +76,11 @@ function renderFile(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);
+
+  if (isIgnorePaths(fullSourceFile, context)) {
+    return;
+  }
+
   console.log(green(`+ ${ fullTargetFile }`));
 
   if (isCopyOnlyPath(fullSourceFile, context)) {
@@ -88,6 +105,10 @@ function renderDir(dir, context) {
   const fullSourceDir = path.resolve(context.repoDir, dir);
   const fullTargetDir = path.resolve(context.outputDir, renderedDir);
 
+  if (isIgnorePaths(fullSourceDir, context)) {
+    return;
+  }
+
   debug('Source Dir: %s, target dir: %s', fullSourceDir, fullTargetDir);
   console.log(green(`+ ${ fullTargetDir }`));
   if (!fs.existsSync(fullTargetDir)) {
diff --git a/package-lock.json b/package-lock.json
index e8d69e4..d276418 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -138,7 +138,7 @@
     },
     "@sinonjs/formatio": {
       "version": "2.0.0",
-      "resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz",
+      "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz",
       "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==",
       "dev": true,
       "requires": {
@@ -300,7 +300,7 @@
     },
     "array-from": {
       "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/array-from/download/array-from-2.1.1.tgz",
       "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
       "dev": true
     },
@@ -1965,7 +1965,7 @@
     },
     "git-ignore-parser": {
       "version": "0.0.2",
-      "resolved": "https://registry.npmjs.org/git-ignore-parser/-/git-ignore-parser-0.0.2.tgz",
+      "resolved": "http://registry.npm.taobao.org/git-ignore-parser/download/git-ignore-parser-0.0.2.tgz",
       "integrity": "sha1-fykXBKrv9mlvmHePzLJ2AbtzOTI="
     },
     "glob": {


Test patch:
diff --git a/test/init/context.test.js b/test/init/context.test.js
index 1d1f5b0..d389c56 100644
--- a/test/init/context.test.js
+++ b/test/init/context.test.js
@@ -85,5 +85,66 @@ describe('context', () => {
     });
   });
 
+  it('build context when copyOnlyPaths is foo', async () => { 
+    config.getConfig.returns({ name: 'foo', copyOnlyPaths: 'foo'});
+    renderer.renderContent.returns('foo');
+    fs.readdirSync.returns(['bar', '{{ foo }}']);
+    const context = {outputDir: '/', vars: {}};
+    await contextStub.buildContext('foo', context);
+    expect(context).to.eql({
+      outputDir: '/',
+      repoDir: 'foo',
+      templateDir: '{{ foo }}',
+      config: { name: 'foo' , copyOnlyPaths: 'foo'},
+      vars: { projectName: 'fun-app' }
+    });
+  });
+
+  it('build context when copyOnlyPaths is ["foo", "bar"]', async () => { 
+    config.getConfig.returns({ name: 'foo', copyOnlyPaths: ['foo', 'bar']});
+    renderer.renderContent.returns('foo');
+    fs.readdirSync.returns(['bar', '{{ foo }}']);
+    const context = {outputDir: '/', vars: {}};
+    await contextStub.buildContext('foo', context);
+    expect(context).to.eql({
+      outputDir: '/',
+      repoDir: 'foo',
+      templateDir: '{{ foo }}',
+      config: { name: 'foo' , copyOnlyPaths: 'foo\nbar'},
+      vars: { projectName: 'fun-app' }
+    });
+  });
+
+  it('build context when ignorePaths is foo', async () => { 
+    config.getConfig.returns({ name: 'foo', ignorePaths: 'foo'});
+    renderer.renderContent.returns('foo');
+    fs.readdirSync.returns(['bar', '{{ foo }}']);
+    const context = {outputDir: '/', vars: {}};
+    await contextStub.buildContext('foo', context);
+    expect(context).to.eql({
+      outputDir: '/',
+      repoDir: 'foo',
+      templateDir: '{{ foo }}',
+      config: { name: 'foo' , ignorePaths: 'foo'},
+      vars: { projectName: 'fun-app' }
+    });
+  });
+
+  it('build context when ignorePaths is ["foo", "bar"]', async () => { 
+    config.getConfig.returns({ name: 'foo', ignorePaths: ['foo', 'bar']});
+    renderer.renderContent.returns('foo');
+    fs.readdirSync.returns(['bar', '{{ foo }}']);
+    const context = {outputDir: '/', vars: {}};
+    await contextStub.buildContext('foo', context);
+    expect(context).to.eql({
+      outputDir: '/',
+      repoDir: 'foo',
+      templateDir: '{{ foo }}',
+      config: { name: 'foo' , ignorePaths: 'foo\nbar'},
+      vars: { projectName: 'fun-app' }
+    });
+  });
+
+
 
 });
\ No newline at end of file
diff --git a/test/init/renderer.test.js b/test/init/renderer.test.js
index 1bc8203..85cf7e3 100644
--- a/test/init/renderer.test.js
+++ b/test/init/renderer.test.js
@@ -96,16 +96,18 @@ describe('renderer', () => {
   });
 
   it('render', async () => {
-    fs.readdirSync.withArgs('baz').returns(['foo', 'bar', 'abc']).withArgs('foo').returns([]);
+    fs.readdirSync.withArgs('baz').returns(['foo', 'bar', 'abc', 'cba']).withArgs('foo').returns([]);
     fs.createReadStream.returns({ pipe: () => {} });
     fs.statSync
       .withArgs('foo').returns({ isDirectory: () => true })
       .withArgs('bar').returns({ isDirectory: () => false })
+      .withArgs('cba').returns({ isDirectory: () => false })
       .withArgs('abc').returns({ isDirectory: () => false });
     fs.readFileSync.returns('test {{ foo }}');
-    rendererStub.render({ repoDir: 'foor', templateDir: 'baz', vars: { foo: 'bar' }, config: { copyOnlyPaths: 'abc' } });
+    rendererStub.render({ repoDir: 'foor', templateDir: 'baz', vars: { foo: 'bar' }, config: { copyOnlyPaths: 'abc', ignorePaths: 'cba' } });
     sandbox.assert.calledWith(fs.mkdirSync, 'foo');
     sandbox.assert.calledWith(fs.writeFileSync, 'bar', 'test bar');
+    sandbox.assert.neverCalledWith(fs.writeFileSync, 'cba', 'test bar');
     sandbox.assert.calledOnce(fs.writeFileSync);
     sandbox.assert.calledOnce(fs.createReadStream);
     sandbox.assert.calledWith(fs.createReadStream, 'abc');
@@ -113,6 +115,31 @@ describe('renderer', () => {
     sandbox.assert.calledWith(fs.createWriteStream, 'abc');
   });
 
+  it('render with multi path', async () => {
+    fs.readdirSync.withArgs('baz').returns(['foo', 'bar', 'abc', 'abc2', 'cba2']).withArgs('foo').returns([]);
+    fs.createReadStream.returns({ pipe: () => {} });
+    fs.statSync
+      .withArgs('foo').returns({ isDirectory: () => true })
+      .withArgs('bar').returns({ isDirectory: () => false })
+      .withArgs('cba').returns({ isDirectory: () => false })
+      .withArgs('cba2').returns({ isDirectory: () => false })
+      .withArgs('abc').returns({ isDirectory: () => false })
+      .withArgs('abc2').returns({ isDirectory: () => false });
+    fs.readFileSync.returns('test {{ foo }}');
+    rendererStub.render({ repoDir: 'foor', templateDir: 'baz', vars: { foo: 'bar' }, config: { copyOnlyPaths: 'abc\nabc2', ignorePaths: 'cba\ncba2' } });
+    sandbox.assert.calledWith(fs.mkdirSync, 'foo');
+    sandbox.assert.calledWith(fs.writeFileSync, 'bar', 'test bar');
+    sandbox.assert.neverCalledWith(fs.writeFileSync, 'cba', 'test bar');
+    sandbox.assert.neverCalledWith(fs.writeFileSync, 'cba2', 'test bar');
+    sandbox.assert.calledOnce(fs.writeFileSync);
+    sandbox.assert.calledTwice(fs.createReadStream);
+    sandbox.assert.calledTwice(fs.createWriteStream);
+    sandbox.assert.calledWith(fs.createReadStream, 'abc');
+    sandbox.assert.calledWith(fs.createWriteStream, 'abc');
+    sandbox.assert.calledWith(fs.createReadStream, 'abc2');
+    sandbox.assert.calledWith(fs.createWriteStream, 'abc2');
+  });
+
   it('render when merge is true', async () => {
     fs.readdirSync.withArgs('baz').returns(['template.yml']);
     fs.existsSync.returns(true);
@@ -124,5 +151,4 @@ describe('renderer', () => {
     sandbox.assert.calledWith(yaml.safeDump, realYaml.safeLoad(mergedTemplate));
   });
 
-
 });
\ No newline at end of file