preface

Book Connect back to Vee-CLI Scaffolding Practices (I)

Last time I introduced some distribution configuration of script commands. This article mainly introduces the template pull and copy of creating files, mainly the specific logic under create.js

Template pull

Depend on the package

[Package directory structure]

  • Axios (send the request, promise encapsulation)
  • Ora (waiting for loading)
  • Inquirer (Ask select command)
  • Download git-repo (pull repository from Github)
  • NCP (copy files to specified directory)

[Directory Description] Send a request via AXIos to get the repository and version number on Github (ps: Ora-git-repo will download the repository to the corresponding directory, usually.template, and then copy the downloaded files directly to the specified directory via NCP

Logic code

Get the repository and version

const fetchRepoList = async () => {
    const { data } = await axios.get(`${repoUrl}/repos`);
    return data;
}

const fetchTagList = async (repo) => {
    const { data } = await axios.get(`${tagUrl}/${repo}/tags`);
    return data;
}
Copy the code

All the fixed parameters can be put into constants.js for derivation

functions

const waitLoading = (fn, message) => async (... args) => { const spinner = ora(message); spinner.start(); const result = await fn(... args); spinner.succeed();return result;
}

const download = async (repo, tag) => {
    let api = `${baseUrl}/${repo}`;
    if(tag) {
        api += `#${tag}`;
    }
    const dest = `${downloadDirectory}/${repo}`;
    await downloadGitRepoPro(api, dest);
    return dest;
}
Copy the code

The function to download the Github repository is written as a callback and we want to return it as a promise so that async/await code can be written using promisify(ps: This function is often asked in the interview, how to implement promisify programming (how to implement Promisify programming), in addition, it uses the higher order functions of the function Currization (ps: Function Currization and anti-Currization) for multiple function arguments.

Export module

Module.exports = async (projectName) => {// get repository const repos = awaitwaitLoading(fetchRepoList, ' fetching template ... ') (); const reposName = repos.map( item => item.name ); const { repo } = await Inquirer.prompt({ name:'repo'.type: 'list',
        message: 'please choice a template to create project', Choices: reposName}) // Get version number const tags = awaitwaitLoading(fetchTagList, ' fetching tags ... ')(repo);
    const tagsName = tags.map( item => item.name );
    const { tag } = await Inquirer.prompt({
        name: 'tag'.type: 'list',
        message: 'please choice a template to create project',
        choices: tagsName
    });

    const result = await waitLoading(download, 'download template ... ')(repo,tag); console.log(result); // Download await ncpPro(result, path.resolve(projectName)); // copy template after rendering}Copy the code

Here, the pulled template is downloaded directly to the current directory, and the parts that need to be compiled will be described in the next article

Related package source code analysis

There are many packages designed in this paper. Due to the limited space, the core code highlights of several core packages are selected for analysis

ora

class Ora {
	constructor(options) {
		if (typeof options === 'string') {
			options = {
				text: options
			};
		}

		this.options = {
			text: ' ',
			color: 'cyan',
			stream: process.stderr,
			discardStdin: true. options }; this.spinner = this.options.spinner; this.color = this.options.color; this.hideCursor = this.options.hideCursor ! = =false;
		this.interval = this.options.interval || this.spinner.interval || 100;
		this.stream = this.options.stream;
		this.id = undefined;
		this.isEnabled = typeof this.options.isEnabled === 'boolean' ? this.options.isEnabled : isInteractive({stream: this.stream});

		// Set *after* `this.stream`
		this.text = this.options.text;
		this.prefixText = this.options.prefixText;
		this.linesToClear = 0;
		this.indent = this.options.indent;
		this.discardStdin = this.options.discardStdin;
		this.isDiscardingStdin = false;
	}

	get indent() {
		return this._indent;
	}

	set indent(indent = 0) {
		if(! (indent >= 0 && Number.isInteger(indent))) { throw new Error('The `indent` option must be an integer from 0 and up');
		}

		this._indent = indent;
	}

	_updateInterval(interval) {
		if(interval ! == undefined) { this.interval = interval; } } getspinner() {
		return this._spinner;
	}

	set spinner(spinner) {
		this.frameIndex = 0;

		if (typeof spinner === 'object') {
			if (spinner.frames === undefined) {
				throw new Error('The given spinner must have a `frames` property');
			}

			this._spinner = spinner;
		} else if (process.platform === 'win32') {
			this._spinner = cliSpinners.line;
		} else if (spinner === undefined) {
			// Set default spinner
			this._spinner = cliSpinners.dots;
		} else if (cliSpinners[spinner]) {
			this._spinner = cliSpinners[spinner];
		} else {
			throw new Error(`There is no built-in spinner named '${spinner}'. See https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json for a full list.`);
		}

		this._updateInterval(this._spinner.interval);
	}

	get text() {
		return this[TEXT];
	}

	get prefixText() {
		return this[PREFIX_TEXT];
	}

	get isSpinning() {
		returnthis.id ! == undefined; }updateLineCount() {
		const columns = this.stream.columns || 80;
		const fullPrefixText = (typeof this[PREFIX_TEXT] === 'string')? this[PREFIX_TEXT] +The '-' : ' ';
		this.lineCount = stripAnsi(fullPrefixText + The '-' + this[TEXT]).split('\n').reduce((count, line) => {
			return count + Math.max(1, Math.ceil(wcwidth(line) / columns));
		}, 0);
	}

	set text(value) {
		this[TEXT] = value;
		this.updateLineCount();
	}

	set prefixText(value) {
		this[PREFIX_TEXT] = value;
		this.updateLineCount();
	}

	frame() {
		const {frames} = this.spinner;
		let frame = frames[this.frameIndex];

		if (this.color) {
			frame = chalk[this.color](frame);
		}

		this.frameIndex = ++this.frameIndex % frames.length;
		const fullPrefixText = (typeof this.prefixText === 'string'&& this.prefixText ! = =' ')? this.prefixText +' ' : ' ';
		const fullText = typeof this.text === 'string' ? ' ' + this.text : ' ';

		return fullPrefixText + frame + fullText;
	}

	clear() {
		if(! this.isEnabled || ! this.stream.isTTY) {return this;
		}

		for (let i = 0; i < this.linesToClear; i++) {
			if (i > 0) {
				this.stream.moveCursor(0, -1);
			}

			this.stream.clearLine();
			this.stream.cursorTo(this.indent);
		}

		this.linesToClear = 0;

		return this;
	}

	render() {
		this.clear();
		this.stream.write(this.frame());
		this.linesToClear = this.lineCount;

		return this;
	}

	start(text) {
		if (text) {
			this.text = text;
		}

		if(! this.isEnabled) {if (this.text) {
				this.stream.write(`- ${this.text}\n`);
			}

			return this;
		}

		if (this.isSpinning) {
			return this;
		}

		if (this.hideCursor) {
			cliCursor.hide(this.stream);
		}

		if (this.discardStdin && process.stdin.isTTY) {
			this.isDiscardingStdin = true;
			stdinDiscarder.start();
		}

		this.render();
		this.id = setInterval(this.render.bind(this), this.interval);

		return this;
	}

	stop() {
		if(! this.isEnabled) {return this;
		}

		clearInterval(this.id);
		this.id = undefined;
		this.frameIndex = 0;
		this.clear();
		if (this.hideCursor) {
			cliCursor.show(this.stream);
		}

		if (this.discardStdin && process.stdin.isTTY && this.isDiscardingStdin) {
			stdinDiscarder.stop();
			this.isDiscardingStdin = false;
		}

		return this;
	}

	succeed(text) {
		return this.stopAndPersist({symbol: logSymbols.success, text});
	}

	fail(text) {
		return this.stopAndPersist({symbol: logSymbols.error, text});
	}

	warn(text) {
		return this.stopAndPersist({symbol: logSymbols.warning, text});
	}

	info(text) {
		return this.stopAndPersist({symbol: logSymbols.info, text});
	}

	stopAndPersist(options = {}) {
		const prefixText = options.prefixText || this.prefixText;
		const fullPrefixText = (typeof prefixText === 'string'&& prefixText ! = =' ')? prefixText +' ' : ' ';
		const text = options.text || this.text;
		const fullText = (typeof text === 'string')?' ' + text : ' ';

		this.stop();
		this.stream.write(`${fullPrefixText}${options.symbol || ' '}${fullText}\n`);

		returnthis; }}Copy the code

The core spin is a SPINners JSON file that maintains an ORA category

inquirer

class StateManager {
  constructor(configFactory, initialState, render) {
    this.initialState = initialState;
    this.render = render;
    this.currentState = {
      loadingIncrement: 0,
      value: ' ',
      status: 'idle'}; // Default `input` to stdin const input = process.stdin; // Add mute capabilities to the output const output = new MuteStream(); output.pipe(process.stdout); this.rl = readline.createInterface({ terminal:true,
      input,
      output,
    });
    this.screen = new ScreenManager(this.rl);

    let config = configFactory;
    if (_.isFunction(configFactory)) {
      config = configFactory(this.rl);
    }

    this.config = config;

    this.onKeypress = this.onKeypress.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.startLoading = this.startLoading.bind(this);
    this.onLoaderTick = this.onLoaderTick.bind(this);
    this.setState = this.setState.bind(this);
    this.handleLineEvent = this.handleLineEvent.bind(this);
  }

  async execute(cb) {
    let { message } = this.getState();
    this.cb = cb;

    // Load asynchronous properties
    const showLoader = setTimeout(this.startLoading, 500);
    if (_.isFunction(message)) {
      message = await runAsync(message)();
    }

    this.setState({ message, status: 'idle' });

    // Disable the loader if it didn't launch clearTimeout(showLoader); // Setup event listeners once we're done fetching the configs
    this.rl.input.on('keypress', this.onKeypress);
    this.rl.on('line', this.handleLineEvent);
  }

  onKeypress(value, key) {
    const { onKeypress = _.noop } = this.config;
    // Ignore enter keypress. The "line" event is handling those.
    if (key.name === 'enter' || key.name === 'return') {
      return;
    }

    this.setState({ value: this.rl.line, error: null });
    onKeypress(this.rl.line, key, this.getState(), this.setState);
  }

  startLoading() {
    this.setState({ loadingIncrement: 0, status: 'loading' });
    setTimeout(this.onLoaderTick, spinner.interval);
  }

  onLoaderTick() {
    const { status, loadingIncrement } = this.getState();
    if (status === 'loading') {
      this.setState({ loadingIncrement: loadingIncrement + 1 });
      setTimeout(this.onLoaderTick, spinner.interval); }}handleLineEvent() {
    const { onLine = defaultOnLine } = this.config;
    onLine(this.getState(), {
      submit: this.onSubmit,
      setState: this.setState,
    });
  }

  async onSubmit() {
    const state = this.getState();
    const { validate, filter } = state;
    const { validate: configValidate = () => true } = this.config;

    const { mapStateToValue = defaultMapStateToValue } = this.config;
    let value = mapStateToValue(state);

    const showLoader = setTimeout(this.startLoading, 500);
    this.rl.pause();
    try {
      const filteredValue = await runAsync(filter)(value);
      let isValid = configValidate(value, state);
      if (isValid === true) {
        isValid = await runAsync(validate)(filteredValue);
      }

      if (isValid === true) {
        this.onDone(filteredValue);
        clearTimeout(showLoader);
        return;
      }

      this.onError(isValid);
    } catch (err) {
      this.onError(err.message + '\n' + err.stack);
    }

    clearTimeout(showLoader);
    this.rl.resume();
  }

  onError(error) {
    this.setState({
      status: 'idle',
      error: error || 'You must provide a valid value'}); } onDone(value) { this.setState({ status:'done' });
    this.rl.input.removeListener('keypress', this.onKeypress);
    this.rl.removeListener('line', this.handleLineEvent);
    this.screen.done();
    this.cb(value);
  }

  setState(partialState) {
    this.currentState = Object.assign({}, this.currentState, partialState);
    this.onChange(this.getState());
  }

  getState() {
    return Object.assign({}, defaultState, this.initialState, this.currentState);
  }

  getPrefix() {
    const { status, loadingIncrement } = this.getState();
    let prefix = chalk.green('? ');
    if (status === 'loading') {
      const frame = loadingIncrement % spinner.frames.length;
      prefix = chalk.yellow(spinner.frames[frame]);
    }

    return prefix;
  }

  onChange(state) {
    const { status, message, value, transformer } = this.getState();

    let error;
    if (state.error) {
      error = `${chalk.red('>>')} ${state.error}`;
    }

    const renderState = Object.assign(
      {
        prefix: this.getPrefix(),
      },
      state,
      {
        // Only pass message down if it's a string. Otherwise we're still in init state
        message: _.isFunction(message) ? 'Loading... ' : message,
        value: transformer(value, { isFinal: status === 'done'}), validate: undefined, filter: undefined, transformer: undefined, } ); this.screen.render(this.render(renderState, this.config), error); }}Copy the code

The input and output of the command line are mainly process.stdin and process.stdout, and then obtained by readline. The core of this is to maintain a StateManager class to obtain and map the selected content

download-git-repo

function download (repo, dest, opts, fn) {
  if (typeof opts === 'function') {
    fn = opts
    opts = null
  }
  opts = opts || {}
  var clone = opts.clone || false
  delete opts.clone

  repo = normalize(repo)
  var url = repo.url || getUrl(repo, clone)

  if (clone) {
    var cloneOptions = {
      checkout: repo.checkout,
      shallow: repo.checkout === 'master'. opts } gitclone(url, dest,cloneOptions, function (err) {
      if (err === undefined) {
        rm(dest + '/.git')
        fn()
      } else {
        fn(err)
      }
    })
  } else {
    var downloadOptions = {
      extract: true,
      strip: 1,
      mode: '666'. opts, headers: { accept:'application/zip'. (opts.headers || {}) } } downloadUrl(url, dest, downloadOptions) .then(function (data) {
        fn()
      })
      .catch(function (err) {
        fn(err)
      })
  }
}
Copy the code

Its core is the download and Git-clone package, where git-clone is the API corresponding to Git JS, through git Clone down the repository to read and write flow operations

ncp

function ncp (source, dest, options, callback) {
  var cback = callback;

  if(! callback) { cback = options; options = {}; } var basePath = process.cwd(), currentPath = path.resolve(basePath,source), targetPath = path.resolve(basePath, dest), filter = options.filter, rename = options.rename, transform = options.transform, clobber = options.clobber ! = =false,
      modified = options.modified,
      dereference = options.dereference,
      errs = null,
      started = 0,
      finished = 0,
      running = 0,
      limit = options.limit || ncp.limit || 16;

  limit = (limit< 1)? 1: (limit> 512)? 512:limit;

  startCopy(currentPath);
  
  function startCopy(source) {
    started++;
    if (filter) {
      if (filter instanceof RegExp) {
        if(! filter.test(source)) {
          return cb(true); }}else if (typeof filter === 'function') {
        if(! filter(source)) {
          return cb(true); }}}return getStats(source);
  }

  function getStats(source) {
    var stat = dereference ? fs.stat : fs.lstat;
    if (running >= limit) {
      return setImmediate(function () {
        getStats(source);
      });
    }
    running++;
    stat(source.function (err, stats) {
      var item = {};
      if (err) {
        return onError(err);
      }

      // We need to get the mode from the stats object and preserve it.
      item.name = source;
      item.mode = stats.mode;
      item.mtime = stats.mtime; //modified time
      item.atime = stats.atime; //access time

      if (stats.isDirectory()) {
        return onDir(item);
      }
      else if (stats.isFile()) {
        return onFile(item);
      }
      else if (stats.isSymbolicLink()) {
        // Symlinks don't really need to know about the mode. return onLink(source); }}); } function onFile(file) { var target = file.name.replace(currentPath, targetPath); if(rename) { target = rename(target); } isWritable(target, function (writable) { if (writable) { return copyFile(file, target); } if(clobber) { rmFile(target, function () { copyFile(file, target); }); } if (modified) { var stat = dereference ? fs.stat : fs.lstat; stat(target, function(err, stats) { //if souce modified time greater to target modified time copy file if (file.mtime.getTime()>stats.mtime.getTime()) copyFile(file, target); else return cb(); }); } else { return cb(); }}); } function copyFile(file, target) { var readStream = fs.createReadStream(file.name), writeStream = fs.createWriteStream(target, { mode: file.mode }); readStream.on('error', onError); writeStream.on('error', onError); if(transform) { transform(readStream, writeStream, file); } else { writeStream.on('open', function() { readStream.pipe(writeStream); }); } writeStream.once('finish', function() {
        if (modified) {
            //target file modified date sync.
            fs.utimesSync(target, file.atime, file.mtime);
            cb();
        }
        else cb();
    });
  }

  function rmFile(file, done) {
    fs.unlink(file, function (err) {
      if (err) {
        return onError(err);
      }
      return done();
    });
  }

  function onDir(dir) {
    var target = dir.name.replace(currentPath, targetPath);
    isWritable(target, function (writable) {
      if (writable) {
        return mkDir(dir, target);
      }
      copyDir(dir.name);
    });
  }

  function mkDir(dir, target) {
    fs.mkdir(target, dir.mode, function (err) {
      if (err) {
        return onError(err);
      }
      copyDir(dir.name);
    });
  }

  function copyDir(dir) {
    fs.readdir(dir, function (err, items) {
      if (err) {
        return onError(err);
      }
      items.forEach(function (item) {
        startCopy(path.join(dir, item));
      });
      return cb();
    });
  }

  function onLink(link) {
    var target = link.replace(currentPath, targetPath);
    fs.readlink(link, function (err, resolvedPath) {
      if (err) {
        return onError(err);
      }
      checkLink(resolvedPath, target);
    });
  }

  function checkLink(resolvedPath, target) {
    if (dereference) {
      resolvedPath = path.resolve(basePath, resolvedPath);
    }
    isWritable(target, function (writable) {
      if (writable) {
        return makeLink(resolvedPath, target);
      }
      fs.readlink(target, function (err, targetDest) {
        if (err) {
          return onError(err);
        }
        if (dereference) {
          targetDest = path.resolve(basePath, targetDest);
        }
        if (targetDest === resolvedPath) {
          return cb();
        }
        return rmFile(target, function () {
          makeLink(resolvedPath, target);
        });
      });
    });
  }

  function makeLink(linkPath, target) {
    fs.symlink(linkPath, target, function (err) {
      if (err) {
        return onError(err);
      }
      return cb();
    });
  }

  function isWritable(path, done) {
    fs.lstat(path, function (err) {
      if (err) {
        if (err.code === 'ENOENT') return done(true); return done(false); } return done(false); }); } function onError(err) { if (options.stopOnError) { return cback(err); } else if (! errs && options.errs) { errs = fs.createWriteStream(options.errs); } else if (! errs) { errs = []; } if (typeof errs.write === 'undefined') { errs.push(err); } else { errs.write(err.stack + '\n\n'); } return cb(); } function cb(skipped) { if (! skipped) running--; finished++; if ((started === finished) && (running === 0)) { if (cback ! == undefined ) { return errs ? cback(errs) : cback(null); }}}}Copy the code

Path and FS module core application, for the application of reading and writing files students can refer to the writing method

conclusion

This article mainly describes template pulling and copying. How to write complex templates that need to be compiled

To be continued…

reference

  • Ora. Js source code
  • Inquirer. Js source code
  • Inquirer. Js – a tool for users to interact with the command line
  • Download git – repo. Js source code
  • NCP. Js source code