ember-cli/ember-cli

View on GitHub
dev/update-blueprint-dependencies.js

Summary

Maintainability
A
1 hr
Test Coverage
'use strict';

function usage() {
  console.log(`This script updates the dependencies / devDependencies in the main addon and app blueprints along with their corresponding test fixtures.

Options:

  - '--ember-source' (required) - The dist-tag to use for ember-source
  - '--ember-data' (required) - The dist-tag to use for ember-data
  - '--filter' (optional) - A RegExp to filter the packages to update by
  - '--latest' (optional) - Always use the latest version available for a package (includes major bumps, 'false' by default)

Example:

node dev/update-blueprint-dependencies.js --ember-source=beta --ember-data=beta

node dev/update-blueprint-dependencies.js --filter /eslint/

node dev/update-blueprint-dependencies.js --filter some-package@beta
`);
}

const fs = require('fs');
const path = require('path');
const util = require('util');
const nopt = require('nopt');
const npmPackageArg = require('npm-package-arg');
const _latestVersion = require('latest-version');

nopt.typeDefs['regexp'] = {
  type: RegExp,
  validate(data, key, value) {
    let regexp = new RegExp(value);

    data[key] = regexp;
  },
};

const OPTIONS = nopt({
  'ember-source': String,
  'ember-data': String,
  filter: String,
  latest: Boolean,
});

const PACKAGE_FILES = [
  '../blueprints/app/files/package.json',
  '../blueprints/addon/additional-package.json',
  '../tests/fixtures/app/defaults/package.json',
  '../tests/fixtures/app/npm/package.json',
  '../tests/fixtures/app/yarn/package.json',
  '../tests/fixtures/app/pnpm/package.json',
  '../tests/fixtures/app/embroider/package.json',
  '../tests/fixtures/app/embroider-yarn/package.json',
  '../tests/fixtures/app/embroider-pnpm/package.json',
  '../tests/fixtures/app/embroider-no-welcome/package.json',
  '../tests/fixtures/app/typescript/package.json',
  '../tests/fixtures/app/typescript-embroider/package.json',
  '../tests/fixtures/addon/defaults/package.json',
  '../tests/fixtures/addon/yarn/package.json',
  '../tests/fixtures/addon/pnpm/package.json',
  '../tests/fixtures/addon/typescript/package.json',
];

let filter = {
  nameRegexp: null,
  name: null,
};

if (OPTIONS.filter) {
  if (OPTIONS.filter.startsWith('/')) {
    filter.nameRegexp = new RegExp(OPTIONS.filter.substring(1, OPTIONS.filter.length - 1));
    // can only use latest when using a regexp style
    filter.fetchSpec = 'latest';
  } else {
    let packageArgResult = npmPackageArg(OPTIONS.filter);
    filter.name = packageArgResult.name;
    OPTIONS[packageArgResult.name] = filter.fetchSpec = packageArgResult.fetchSpec;
  }
}

function shouldCheckDependency(dependency) {
  if (filter.nameRegexp) {
    return filter.nameRegexp.test(dependency);
  }

  if (filter.name) {
    return dependency === filter.name;
  }

  return true;
}

const LATEST = new Map();
async function latestVersion(packageName, semverRange) {
  let result = LATEST.get(packageName);

  if (result === undefined) {
    let options = {
      version: semverRange,
    };

    if (OPTIONS[packageName]) {
      options.version = OPTIONS[packageName];
    }

    result = _latestVersion(packageName, options);
    LATEST.set(packageName, result);
  }

  return result;
}

async function updateDependencies(dependencies) {
  for (let dependencyKey in dependencies) {
    let dependencyName = removeTemplateExpression(dependencyKey);

    if (!shouldCheckDependency(dependencyName)) {
      continue;
    }

    let previousValue = dependencies[dependencyKey];

    // grab the first char (~ or ^)
    let prefix = previousValue[0];
    let isValidPrefix = prefix === '~' || prefix === '^';

    // handle things from blueprints/app/files/package.json like `^2.4.0<% if (welcome) { %>`
    let templateSuffix = previousValue.includes('<') ? previousValue.slice(previousValue.indexOf('<')) : '';

    // check if we are dealing with `~<%= emberCLIVersion %>`
    let hasVersion = previousValue[1] !== '<';

    if (hasVersion && isValidPrefix) {
      const semverRange = OPTIONS.latest ? 'latest' : removeTemplateExpression(previousValue);
      const newVersion = await latestVersion(dependencyName, semverRange);

      dependencies[dependencyKey] = `${prefix}${newVersion}${templateSuffix}`;
    }
  }
}

function removeTemplateExpression(dependency) {
  if (dependency.includes('<') === false) {
    return dependency;
  }

  return dependency.replace(dependency.substring(dependency.indexOf('<'), dependency.indexOf('>') + 1), '');
}

async function main() {
  for (let packageFile of PACKAGE_FILES) {
    let filePath = path.join(__dirname, packageFile);
    let pkg = JSON.parse(fs.readFileSync(filePath, { encoding: 'utf8' }));

    await updateDependencies(pkg.dependencies);
    await updateDependencies(pkg.devDependencies);

    let output = `${JSON.stringify(pkg, null, 2)}\n`;

    fs.writeFileSync(filePath, output, { encoding: 'utf8' });
  }
}

if (module === require.main) {
  // ensure promise rejection is a failure
  process.on('unhandledRejection', (error) => {
    if (!(error instanceof Error)) {
      error = new Error(`Promise rejected with value: ${util.inspect(error)}`);
    }

    console.error(error.stack);

    // eslint-disable-next-line n/no-process-exit
    process.exit(1);
  });

  if (OPTIONS.filter || (OPTIONS['ember-source'] && OPTIONS['ember-data'])) {
    main();
  } else {
    usage();
    process.exitCode = 1;
    return;
  }
}

module.exports = { updateDependencies };