[source code] update notifier detects whether the npm package is updated ~

Keywords: Javascript node.js Vue.js

Learning objectives
  • Understand the role and usage scenario of update notifier
  • Application scenario:
    Check whether npm packages are updated, such as component library updates or other npm package updates,
Data preparation

Link: https://github.com/yeoman/update-notifier

What is an update notifier

Prompt package needs to be updated

The essence is to open the child_process runs in the background by comparing the name and version in package.json with the latest version in the current npm registry. If an updated version is found available, the result will be saved in the. Update attribute to mark whether an updated version is available. If so, update information will be output and the updated version is available

preparation in advance
git clone https://github.com/yeoman/update-notifier
cd update-notifier
npm i
First knowledge - Official Documents
  1. usage
  • simple
    const updateNotifier = require('update-notifier');
	const pkg = require('./package.json');
	
	updateNotifier({pkg}).notify();
  • Comprehensive
	const updateNotifier = require('update-notifier');
	const pkg = require('./package.json');
		
	// Checks for available update and returns an instance
	const notifier = updateNotifier({pkg});
	
	// Notify using the built-in convenience method
	notifier.notify();
	
	// `notifier.update` contains some useful info about the update
	console.log(notifier.update);
	/*
	{
		latest: '1.0.1',
		current: '1.0.0',
		type: 'patch', // Possible values: latest, major, minor, patch, prerelease, build
		name: 'pageres'
	}
	*/
	
  • Options and custom messages
    const notifier = updateNotifier({
    	pkg,
    	updateCheckInterval: 1000 * 60 * 60 * 24 * 7 // 1 week
    });
    
    if (notifier.update) {
    	console.log(`Update available: ${notifier.update.latest}`);
    }
    
  1. HOW

    When the user runs the application for the first time, it will check whether there are updates. Even if there are updates available, it will wait for the specified updateCheckInterval before notifying the user. This is done so as not to disturb the user.

  2. API

    • notifier = updateNotifier(options)
      Check for updates available. Accept the options defined below. If an update is available, an instance with the. Update property is returned; otherwise, an undefined instance is returned.
          options:  {
       			pkg: {}, // object type
       			name: '', //Required, string type
       			version: '', //Required, string type
       			updateCheckInterval: '',//number type, the default value is one day (1000 * 60 * 60 * 24)
       			shouldNotifyInNpmScript: false,//Boolean value. The default value is false. Whether to allow notifications to be displayed when running as npm scripts
       			distTag: 'latest'//String, the default value is latest
       	}
      
    • notifier.fetchInfo()
      Check the update information and return an object with the following contents
      {
        latest: '' ,//String, latest version
        current: '',//String, current version
        type: '',//String, the type of current update. The possible values are: latest, major, minor, patch, prerelease, build
        name: '' // String, package name
       }
      
    • notifier.notify(options?)
      A convenient way to display notification messages, only when there are updates and the process is TTY. options:
          {
              defer: true, // Boolean value, the default is true, and the notification will be delayed after the process exits
              message: '', // string, the message that will be displayed when the update is available
              isGlobal:true/false, // Boolean value, including the - g parameter in the npm i recommendation of the default message.
              boxenOptions: object,// An object, the parameter passed to box
           }
      
Interpreting index.js

Reference package section

  1. const {spawn} = require('child_process');
  • Kill the executing child process in node.js
  • const {spawn} = require('child_process'); And const spawn = require('child_process'); Differences between:
    1. {x} = v equals {x:x} = v
    2. {x:y} = v is equivalent to y = v.x
    3. const {spawn} = require('child_process'); Equivalent to const_= require('child_process'); const spawn = _. resolve;
    4. It is equivalent to the deconstructed thing. With this thing in the current file, it has been obtained and can be used directly. There is no need to get the corresponding variables or methods through spawn.xxxx
  1. const path = require('path');

node.js deals with the problem of file path link

  1. const {format} = require('util');
  • util is a common function set, which is used to make up for the lack of too concise core JS functions, in order to meet the requirements of the internal api of nide.js
  • util.format(format[, ...])
    1. The util.form() function takes a formatted string format as the first parameter and returns the formatted string.
    2. The format parameter is a string that can contain zero or more placeholders. Each placeholder starts with a% character and is eventually replaced by the string value converted by the corresponding parameter
  1. const importLazy = require('import-lazy')(require); usage
const importLazy = require('import-lazy')(require);//Module lazy loading
const configstore = importLazy('configstore');// Easily load and persist configurations regardless of where and how they are loaded.
const chalk = importLazy('chalk'); //Color font notification
const semver = importLazy('semver'); //Semantic versioner of npm
const semverDiff = importLazy('semver-diff');// Obtain the difference types of two semver versions / / for example: 0.0.1, 0.0.2 → patch
const latestVersion = importLazy('latest-version');// Get the latest version of npm package
const isNpm = importLazy('is-npm');//Check if the code is running as an npm script
const isInstalledGlobally = importLazy('is-installed-globally'); //Check if the package is installed globally through npm
const isYarnGlobal = importLazy('is-yarn-global'); // Check if it is installed globally by yarn without any fs calls
const hasYarn = importLazy('has-yarn'); //Check if the project is using yarn
const boxen = importLazy('boxen');//Create box at terminal
const xdgBasedir = importLazy('xdg-basedir');//Get XDG base directory path
const isCi = importLazy('is-ci');//Returns true if the current environment is a continuous integration server
const pupa = importLazy('pupa');//Simple micro template

const ONE_DAY = 1000 * 60 * 60 * 24; //24 hours 60 minutes 60 seconds 1000 milliseconds
UpdateNotifier
class UpdateNotifier {
  constructor(options = {}) { }
  check(){}
  async fetchInfo() {}
  notify(options) {}
}
consructor initialization phase
class UpdateNotifier {
	constructor(options = {}) {
		// Give package.json to options
		this.options = options;
		// fault tolerant 
		options.pkg = options.pkg || {};
		options.distTag = options.distTag || 'latest';

		// Reduce pkg to the essential keys. with fallback to deprecated options
		// TODO: Remove deprecated options at some point far into the future
		options.pkg = {
			name: options.pkg.name || options.packageName,
			version: options.pkg.version || options.packageVersion
		};
		//Necessary field detection: an error is reported if the name and version do not exist
		if (!options.pkg.name || !options.pkg.version) {
			throw new Error('pkg.name and pkg.version required');
		}
		//Package name
		this.packageName = options.pkg.name;
		//edition
		this.packageVersion = options.pkg.version;

		/**
		 * By reading the official document, you can know that the updateCheckInterval defaults to one day (the default is in digital format). The inspection cycle here is in ms
		 * const ONE_DAY = 1000 * 60 * 60 * 24; //24 Hours, 60 minutes, 60 seconds, 1000 milliseconds
		 */
		this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;

		/**
		 * isCi()  Returns true if the current environment is a continuous integration server
		 * Disable status detection: disable update notifications
		 */
		this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
			process.env.NODE_ENV === 'test' ||
			process.argv.includes('--no-update-notifier') ||
			isCi();
		/**
		 * shouldNotifyInNpmScript Allow notifications to be displayed when running as npm scripts
		 */
		this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;

		if (!this.disabled) {
			// If update notifications are not disabled
			try {
				//Set configuration object
				const ConfigStore = configstore();
				// Use the configstore to obtain the local storage of the package related information. It is not available for the first time
				this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
					optOut: false,
					// Init with the current time so the first check is only
					// after the set interval, so not to bother users right away
					// Setting the current time as the starting point of the inspection interval does not affect the user. Here is the reason why the user is not notified when starting for the first time
					lastUpdateCheck: Date.now()
				});
			} catch {
				// Expecting error code EACCES or EPERM
				// Prompt error message
				const message =
					chalk().yellow(format(' %s update check failed ', options.pkg.name)) +
					format('\n Try running with %s or get access ', chalk().cyan('sudo')) +
					'\n to the local update config store via \n' +
					chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));

				process.on('exit', () => {
					console.error(boxen()(message, {align: 'center'}));
				});
			}
		}
	}
	
    check() {}
    async fetchInfo() {}
    notify(options) {}
}
check
	check() {
		/**
		 * If the local storage of the package related information cannot be obtained 𞓜 optOut is true | the check is stopped when the update notification is prohibited
		 */
		if (!this.config || this.config.get('optOut') || this.disabled) {
			return;
		}
		//Get the update information of related packages. For the first time, it is undefined
		this.update = this.config.get('update');


		if (this.update) {
			// Use the real latest version instead of the cached one
			//If the latest package information is obtained, the actual value is used instead of the cache value
			this.update.current = this.packageVersion;

			// Clear cached information clear cached information
			this.config.delete('update');
		}

		// Only check for updates on a set interval
		//Check the update only within the set time interval. If it is still in the cycle, return and stop the check
		if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
			return;
		}

		// Spawn a detached process, passing the options as an environment property
		/**
		 * Generate separate processes and pass options as environment properties
		 * path.join Merge multiple parameter values into one path
		 */
		spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
			//Let the child process run independently of its parent process. The child process can continue to run after the parent process exits, whether they are separated or not. [link]( http://nodejs.cn/api/child_process/options_detached.html)
			detached: true,
			// Ignore the input and output of the console and ignore the termination of the parent process
			stdio: 'ignore'
		}).unref();// The unref method is used to break the relationship so that the "parent" process can exit independently (without causing the child process to exit with it)
	}
check.js
/* eslint-disable unicorn/no-process-exit */
'use strict';
// Get the content exported by index.js
let updateNotifier = require('.');

// Get options
const options = JSON.parse(process.argv[2]);
//Instantiate updateNotifier
updateNotifier = new updateNotifier.UpdateNotifier(options);

//Execute a function
(async () => {
	// Exit process when offline
	setTimeout(process.exit, 1000 * 30);

	// Call the fetchInfo method of updateNotifier to get the update information
	const update = await updateNotifier.fetchInfo();

	// Only update the last update check time on success
	updateNotifier.config.set('lastUpdateCheck', Date.now());
	// Set the update field as long as it is not the latest package
	if (update.type && update.type !== 'latest') {
		updateNotifier.config.set('update', update);
	}
	// The obtained information, if the version is updated, is saved to config
	// Call process exit explicitly to terminate the child process,
	// otherwise the child process will run forever, according to the Node.js docs
	// Explicitly call the process exit to terminate the child process. According to the Node.js document, otherwise the child process will run forever

	process.exit();
})().catch(error => {
	console.error(error);
	process.exit(1);
});

fetchInfo gets information about the package
	//Get information about the package
	async fetchInfo() {
		const {distTag} = this.options;
		// latestVersion gets the latest version of the npm package
		// Because it is lazy loading, you need to execute latestVersion() first
		// Note that latestVersion is an asynchronous request
		const latest = await latestVersion()(this.packageName, {version: distTag});
		// Returns the object information after assembly
		return {
			latest,
			current: this.packageVersion,
			type: semverDiff()(this.packageVersion, latest) || distTag,
			name: this.packageName
		};
	}
	notify(options) {}
notify notification
	notify(options) {
		// Support NPM scripts calls
		const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
		/**
		 * !process.stdout.isTTY node Not running on terminal
		 * suppressForNpm NPM scripts calls are not supported
		 * !this.update No updates have been checked locally
		 * !semver().gt(this.update.latest, this.update.current) Is the current version behind the latest version
		 * No notification is required when the above conditions are met (no notification when there is no update)
		 */
		if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver().gt(this.update.latest, this.update.current)) {
			return this;
		}

		options = {
			// Check if the package is installed globally through npm
			isGlobal: isInstalledGlobally(),
			// Is it installed globally through yarn
			// There is a flaw: the premise is that the user does not change the default global installation directory of yarn
			isYarnGlobal: isYarnGlobal()(),
			...options
		};
		// Corresponding update command prompt
		let installCommand;
		if (options.isYarnGlobal) { // Is it a global installation through yarn
			installCommand = `yarn global add ${this.packageName}`;
		} else if (options.isGlobal) { //Is it a global installation through npm
			installCommand = `npm i -g ${this.packageName}`;
		} else if (hasYarn()()) {// hasYarn() because hasYarn is a lazy module, you need to perform a partial installation of hasYarn first
			installCommand = `yarn add ${this.packageName}`;
		} else {// npm local installation
			installCommand = `npm i ${this.packageName}`;
		}

		const defaultTemplate = 'Update available ' +
			chalk().dim('{currentVersion}') +
			chalk().reset(' → ') +
			chalk().green('{latestVersion}') +
			' \nRun ' + chalk().cyan('{updateCommand}') + ' to update';

		//Prompt template
		const template = options.message || defaultTemplate;

		options.boxenOptions = options.boxenOptions || {
			padding: 1,
			margin: 1,
			align: 'center',
			borderColor: 'yellow',
			borderStyle: 'round'
		};
		/**
		 * boxen Create box at terminal
		 * pupa Simple micro template
		 */
		const message = boxen()(
			pupa()(template, {
				// Complete the following information into the template
                // this.update is populated in the check phase
				packageName: this.packageName,
				currentVersion: this.update.current,
				latestVersion: this.update.latest,
				updateCommand: installCommand
			}),
			options.boxenOptions
		);
		//The notification is displayed without delay after the process exits
		if (options.defer === false) {
			console.error(message);
		} else {
			// Display prompt when exiting the process
			process.on('exit', () => {
				console.error(message);
			});
			// Handle unexpected exit
			process.on('SIGINT', () => {
				console.error('');
				process.exit();
			});
		}
		 // Return the purpose of this: the option configuration has been changed in order to chain call
		return this;
	}
Summary process

● const updateNotifier = new UpdateNotifier({})
○ initialize constuctor
○ updateNotifier.check()
● judge whether to execute check.js. If yes:
○ updateNotifier.fetchInfo()
○ set('lastUpdateCheck')
○ set('update')
● notify()

summary

1. Deepen the understanding of deconstruction assignment
2. Know more about the api of node.js according to the source code
3. We also know whether the update notifier update is the tool library of the latest version of the package, which can be added to the actual use
4. It's also a good sequential process to follow the official website, especially when collecting the notes of all the leaders, it will be easier to understand, full of harvest and make persistent efforts!

Posted by mr00047 on Thu, 18 Nov 2021 22:05:13 -0800