meteor/meteor

View on GitHub
npm-packages/meteor-installer/install.js

Summary

Maintainability
C
7 hrs
Test Coverage
const { DownloaderHelper } = require('node-downloader-helper');
const cliProgress = require('cli-progress');
const Seven = require('node-7z');
const path = require('path');
const sevenBin = require('7zip-bin');
const semver = require('semver');
const child_process = require('child_process');
const tmp = require('tmp');
const os = require('os');
const fs = require('fs');

const fsPromises = fs.promises;

const {
  meteorPath,
  release,
  startedPath,
  extractPath,
  isWindows,
  rootPath,
  sudoUser,
  isSudo,
  isMac,
  METEOR_LATEST_VERSION,
  shouldSetupExecPath,
} = require('./config');
const { uninstall } = require('./uninstall');
const {
  extractWithTar,
  extractWith7Zip,
  extractWithNativeTar,
} = require('./extract');
const { engines } = require('./package.json');

const nodeVersion = engines.node;
const npmVersion = engines.npm;

// Compare installed NodeJs version with required NodeJs version
if (!semver.satisfies(process.version, nodeVersion)) {
  console.warn(
    `WARNING: Recommended versions are Node.js ${nodeVersion} and npm ${npmVersion}.`
  );
  console.warn(
    `We recommend using a Node version manager like NVM or Volta to install Node.js and npm.\n`
  );
}

const isInstalledGlobally = process.env.npm_config_global === 'true';

if (!isInstalledGlobally) {
  console.error('******************************************');
  console.error(
    'You are not using a global npm context to install, you should never add meteor to your package.json.'
  );
  console.error('Make sure you pass -g to npm install.');
  console.error('Aborting...');
  console.error('******************************************');
  process.exit(1);
}
process.on('unhandledRejection', err => {
  throw err;
});
if (os.arch() !== 'x64') {
  const isValidM1Version = semver.gte(
    semver.coerce(METEOR_LATEST_VERSION),
    '2.5.1-beta.3'
  );
  if (os.arch() !== 'arm64' || !isMac() || !isValidM1Version) {
    console.error(
      'The current architecture is not supported in this version: ',
      os.arch(),
      '. Try Meteor 2.5.1-beta.3 or above.'
    );
    process.exit(1);
  }
}

const downloadPlatform = {
  win32: 'windows',
  darwin: 'osx',
  linux: 'linux',
};

const url = `https://packages.meteor.com/bootstrap-link?arch=os.${
  downloadPlatform[os.platform()]
}.${os.arch() === 'arm64' ? 'arm64' : 'x86_64'}&release=${release}`;

let tempDirObject;
try {
  tempDirObject = tmp.dirSync();
} catch (e) {
  console.error('');
  console.error('');
  console.error('****************************');
  console.error("Couldn't create tmp dir for extracting meteor.");
  console.error('There are 2 possible causes:');
  console.error(
    '\t1. You are running npm install -g meteor as root without passing the --unsafe-perm option. Please rerun with this option enabled.'
  );
  console.error(
    '\t2. You might not have enough space in disk or permission to create folders'
  );
  console.error('****************************');
  console.error('');
  console.error('');
  process.exit(1);
}
const tempPath = tempDirObject.name;
const tarGzName = 'meteor.tar.gz';
const tarName = 'meteor.tar';

// This file only exists while files are being extracted, and is removed after
// the extraction succeeds. If it still exists, there is either another instance of
// the installer running, or it failed the last time it extracted files.
if (fs.existsSync(startedPath)) {
  console.log('It seems the previous installation of Meteor did not succeed.');
  uninstall();
  console.log('');
} else if (fs.existsSync(meteorPath)) {
  console.log('Meteor is already installed at', meteorPath);
  console.log(
    `If you want to reinstall it, run:

  $ meteor-installer uninstall
  $ meteor-installer install
`
  );
  process.exit();
}

// Creating symlinks requires running as an administrator or
// for developer mode to be enabled
let canCreateSymlinks = false;
try {
  const target = path.resolve(tempPath, 'test-target.txt');
  const symlinkPath = path.resolve(tempPath, 'symlink.txt');

  fs.writeFileSync(target, '');
  fs.symlinkSync(target, symlinkPath, 'file');

  fs.unlinkSync(symlinkPath);
  fs.unlinkSync(target);
  canCreateSymlinks = true;
} catch (e) {
  if (e.code === 'EPERM') {
    // Leave canCreateSymlinks as false
  } else {
    console.error('Unable to check if able to create symlinks');
    console.error(e);
    console.log('Assuming unable to create symlinks');
  }
}

download();

function generateProxyAgent() {
  const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY;
  if (!proxyUrl) {
    return undefined;
  }

  const HttpsProxyAgent = require('https-proxy-agent');

  return new HttpsProxyAgent(proxyUrl);
}

function download() {
  const start = Date.now();
  const downloadProgress = new cliProgress.SingleBar(
    {
      format: 'Downloading |{bar}| {percentage}%',
      clearOnComplete: true,
    },
    cliProgress.Presets.shades_classic
  );
  downloadProgress.start(100, 0);

  const dl = new DownloaderHelper(url, tempPath, {
    retry: { maxRetries: 5, delay: 5000 },
    override: true,
    fileName: tarGzName,
    httpsRequestOptions: {
      agent: generateProxyAgent(),
    },
  });

  dl.on('progress', ({ progress }) => {
    downloadProgress.update(progress);
  });
  dl.on('end', async () => {
    downloadProgress.update(100);
    downloadProgress.stop();
    const end = Date.now();
    console.log(`=> Meteor Downloaded in ${(end - start) / 1000}s`);

    const exists = fs.existsSync(path.resolve(tempPath, tarGzName));
    if (!exists) {
      throw new Error('meteor.tar.gz does not exist');
    }

    if (isWindows()) {
      decompress();
      return;
    }

    fs.writeFileSync(startedPath, 'Meteor install started');
    console.log('=> Extracting the tarball, this may take some time');
    const extractStart = Date.now();
    await extractWithNativeTar(path.resolve(tempPath, tarGzName), extractPath);
    const extractEnd = Date.now();
    console.log(
      `=> Meteor extracted in ${(extractEnd - extractStart) / 1000}s`
    );
    await setup();
  });

  dl.start();
}

function decompress() {
  const start = Date.now();
  const decompressProgress = new cliProgress.SingleBar(
    {
      format: 'Decompressing |{bar}| {percentage}%',
      clearOnComplete: true,
    },
    cliProgress.Presets.shades_classic
  );
  decompressProgress.start(100, 0);

  const myStream = Seven.extract(path.resolve(tempPath, tarGzName), tempPath, {
    $progress: true,
    $bin: sevenBin.path7za,
  });
  myStream.on('progress', function(progress) {
    decompressProgress.update(progress.percent);
  });

  myStream.on('end', function() {
    decompressProgress.update(100);
    decompressProgress.stop();
    const end = Date.now();
    console.log(`=> Meteor Decompressed in ${(end - start) / 1000}s`);
    extract();
  });
}

async function extract() {
  const start = Date.now();
  fs.writeFileSync(startedPath, 'Meteor install started');

  const decompressProgress = new cliProgress.SingleBar(
    {
      format: 'Extracting |{bar}| {percentage}% - {fileCount} files completed',
      clearOnComplete: true,
    },
    cliProgress.Presets.shades_classic
  );
  decompressProgress.start(100, 0, {
    fileCount: 0,
  });

  const tarPath = path.resolve(tempPath, tarName);
  // 7Zip is ~15% faster, but doesn't work when the user doesn't have permission to create symlinks
  // TODO: we could always use 7zip if we have it ignore the symlinks, and then manually create them as
  // is done in extractWithTar
  if (canCreateSymlinks) {
    await extractWith7Zip(tarPath, extractPath, ({ percent, fileCount }) => {
      decompressProgress.update(percent, { fileCount });
    });
  } else {
    await extractWithTar(tarPath, extractPath, ({ percent, fileCount }) => {
      decompressProgress.update(percent, { fileCount });
    });
  }

  decompressProgress.stop();
  const end = Date.now();
  console.log(`=> Meteor Extracted ${(end - start) / 1000}s`);
  await setup();
}
async function setup() {
  fs.unlinkSync(startedPath);
  if (shouldSetupExecPath()) {
    await setupExecPath();
  }
  await fixOwnership();
  showGettingStarted();
}
async function setupExecPath() {
  if (isWindows()) {
    // set for the current session and beyond
    child_process.execSync(`setx path "${meteorPath}/;%path%`);
    return;
  }
  const exportCommand = `export PATH=${meteorPath}:$PATH`;

  const appendPathToFile = async file =>
    fsPromises.appendFile(`${rootPath}/${file}`, `${exportCommand}\n`);

  if (process.env.SHELL && process.env.SHELL.includes('zsh')) {
    await appendPathToFile('.zshrc');
  } else {
    await appendPathToFile('.bashrc');
    await appendPathToFile('.bash_profile');
  }
}
async function fixOwnership() {
  if (!isWindows() && isSudo()) {
    // if we identified sudo is being used, we need to change the ownership of the meteorpath folder
    child_process.execSync(`chown -R ${sudoUser} "${meteorPath}"`);
  }
}

function showGettingStarted() {
  const exportCommand = `export PATH=${meteorPath}:$PATH`;

  const runCommand = isWindows()
    ? `set path "${meteorPath}/;%path%`
    : exportCommand;
  const message = `
***************************************

Meteor has been installed!

To get started fast:

  $ meteor create ~/my_cool_app
  $ cd ~/my_cool_app
  $ meteor

Or see the docs at:

  docs.meteor.com

Deploy and host your app with Cloud:

  www.meteor.com/cloud

***************************************
You might need to open a new terminal window to have access to the meteor command, or run this in your terminal:

${runCommand}

For adding it immediately to your path.
***************************************
  `;

  console.log(message);
}