<commit_before>const _ = require('lodash');
const Bacon = require('baconjs');
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const request = require('request');
const semver = require('semver');
const url = require('url');
const readdirp = require('readdirp');
const rimraf = require('rimraf');
const ssri = require('ssri');
const streamifier = require('streamifier');
const tar = require('tar-fs');
const zlib = require('zlib');

const responseCache = {};
const requiredFiles = {};

function synchronize(options) {

  function sha1(data) {
    const integrity = ssri.fromData(data, {algorithms: ['sha1']});
    return integrity.sha1[0].hexDigest();
  }

  function sha512(data) {
    const integrity = ssri.fromData(data, {algorithms: ['sha512']});
    return integrity.sha512[0].toString();
  }

  function fetchUrl(url, bodyIsBinary) {
    if (responseCache[url]) {
      return Bacon.later(0, responseCache[url]);
    }

    return Bacon.fromNodeCallback(callback => {
      request(url, {timeout: 60000, encoding: bodyIsBinary ? null : undefined},
        (error, response, body) => {
          if (!error && response.statusCode == 200) {
            if (!url.endsWith('.tgz')) {
              responseCache[url] = body;
            }
            callback(null, body);
          } else {
            const statusCode = response ? response.statusCode : 'n/a';
            callback(`Failed to fetch ${url} because of error '${error}' and/or HTTP status ${statusCode}`);
          }
        }
      );
    });
  }

  function encodeScopedPackage(name) {
    return name.replace("\/", "%2f");
  }

  function fetchMetadata(name) {
    return fetchUrl(url.resolve(options.registryUrl, encodeScopedPackage(name))).map(JSON.parse);
  }

  function fetchTarball(dist) {
    return fetchUrl(dist.tarball, true);
  }

  function fetchPrebuiltBinary(name, version, binaryMetadata, abi, platform, arch) {
    return fetchUrl(prebuiltBinaryUrl(name, version, binaryMetadata, abi, platform, arch), true);
  }

  function dependenciesToArray(dependencies) {
    return _(dependencies || {}).keys().map(key => {
      const versionRange = dependencies[key]
      if (versionRange instanceof Array) {
        return versionRange.map(version => {
          return {name: key, versionRange: version};
        });
      }
      return {name: key, versionRange: versionRange};
    }).flatten().value();
  }

  function packageFilename(name, version) {
    const normalized = name.replace('\/', '-');
    return `${normalized}-${version}.tgz`;
  }

  function packageMetadataFilePath(name) {
    mkdirp.sync(path.resolve(options.rootFolder, name));
    return path.resolve(options.rootFolder, path.join(name, 'index.json'));
  }

  function packageTarballFilePath(name, version) {
    return path.resolve(options.rootFolder, path.join(name, packageFilename(name, version)));
  }

  function packageTarballFileUrl(name, version) {
    return url.resolve(options.localUrl, name + '/' + packageFilename(name, version));
  }

  const collectedPackages = {};

  function collectPackage(package) {
    const versions = collectedPackages[package.name] || [];
    if (versions.indexOf(package.version) != -1) {
      return false;
    }
    versions.push(package.version);
    collectedPackages[package.name] = versions;
    return true;
  }

  function collectedPackagesAsArray() {
    return Object.keys(collectedPackages).map(name => ({
        name,
        versions: collectedPackages[name]
      }));
  }

  function resolveVersionAndDependencies(package) {
    return fetchMetadata(package.name)
      .map(metadata => {
          const available = Object.keys(metadata.versions);
          const version = semver.maxSatisfying(available, package.versionRange);
          return {
            name: package.name,
            version: version,
            dependencies: dependenciesToArray(metadata.versions[version].dependencies)
          };
        }
      )
      .flatMap(packageAndDependencies => {
        if (collectPackage(packageAndDependencies)) {
          return Bacon.fromArray(packageAndDependencies.dependencies).flatMapConcat(resolveVersionAndDependencies);
        }
        return Bacon.never();
      });
  }

  function fileExists(file) {
    requiredFiles[file] = true;
    return fs.existsSync(file);
  }

  function tarballExists(distribution) {
    return fileExists(packageTarballFilePath(distribution.name, distribution.version));
  }

  // see node-pre-gyp: /lib/util/versioning.js for documentation of possible values
  function formatPrebuilt(formatString, name, version, moduleName, abi, platform, arch) {
    const moduleVersion = semver.parse(version);
    const prerelease = (moduleVersion.prerelease || []).join('.');
    const build = (moduleVersion.build || []).join('.');

    return formatString
      .replace('{name}', name)
      .replace('{version}', version)
      .replace('{major}', moduleVersion.major)
      .replace('{minor}', moduleVersion.minor)
      .replace('{patch}', moduleVersion.patch)
      .replace('{prerelease}', prerelease)
      .replace('{build}', build)
      .replace('{module_name}', moduleName)
      .replace('{node_abi}', 'node-v' + abi)
      .replace('{platform}', platform)
      .replace('{arch}', arch)
      .replace('{configuration}', 'Release')
      .replace('{toolset}', '');
  }

  function prebuiltBinaryExists(name, version, binaryMetadata, abi, platform, arch) {
    return fileExists(prebuiltBinaryFilePath(name, version, binaryMetadata, abi, platform, arch));
  }

  function prebuiltBinaryFilePath(name, version, binaryMetadata, abi, platform, arch) {
    return path.resolve(options.rootFolder, path.join(name, prebuiltBinaryFileName(name, version, binaryMetadata, abi, platform, arch)));
  }

  function prebuiltBinaryRemotePath(name, version, binaryMetadata, abi, platform, arch) {
    return formatPrebuilt(binaryMetadata.remote_path, name, version, binaryMetadata.module_name, abi, platform, arch).replace(/[\/]+/g, '/');
  }

  function prebuiltBinaryFileName(name, version, binaryMetadata, abi, platform, arch) {
    return formatPrebuilt(binaryMetadata.package_name, name, version, binaryMetadata.module_name, abi, platform, arch).replace(/[\/]+/g, '/');
  }

  function prebuiltBinaryUrl(name, version, binaryMetadata, abi, platform, arch) {
    const remotePath = prebuiltBinaryRemotePath(name, version, binaryMetadata, abi, platform, arch);
    const fileName = prebuiltBinaryFileName(name, version, binaryMetadata, abi, platform, arch);
    return url.resolve(binaryMetadata.host, remotePath + fileName);
  }

  function rewriteMetadataInTarball(distribution, data, callback) {
    const extractDir = path.resolve(options.rootFolder, `_tmp_${distribution.name}`);
    mkdirp.sync(extractDir);
    const tgzIn = streamifier
      .createReadStream(data)
      .pipe(zlib.createGunzip())
      .pipe(tar.extract(extractDir));

    tgzIn.on('finish', () => {
      const packageJsonPath = path.resolve(extractDir, path.join('package', 'package.json'));
      const packagedMetadata = require(packageJsonPath);
      packagedMetadata.binary.host = options.localUrl;
      packagedMetadata.binary.remote_path = `/${distribution.name}/`;
      fs.writeFileSync(packageJsonPath, JSON.stringify(packagedMetadata, null, 2));

      const chunks = [];
      const tgzOut = tar.pack(extractDir).pipe(zlib.createGzip());
      tgzOut.on('data', chunk => chunks.push(chunk));
      tgzOut.on('end', () => {
        rimraf(extractDir, err => {
          callback(err, Buffer.concat(chunks));
        })
      })
      tgzOut.on('error', callback);
    })
    tgzIn.on('error', callback);
  }

  function downloadPackage(nameAndVersions) {
    function cleanupMetadata(metadataContent, versions) {
      const content = _.cloneDeep(metadataContent);
      Object.keys(content.versions).forEach(version => {
        if (versions.indexOf(version) == -1) {
          delete content.versions[version];
          delete content.time[version];
        }
        else {
          content.versions[version].dist.tarball = packageTarballFileUrl(nameAndVersions.name, version);
          if (content.versions[version].binary) {
            content.versions[version].binary.host = options.localUrl;
            content.versions[version].binary.remote_path = `/${nameAndVersions.name}/`;
          }
        }
      });
      content['dist-tags'] = { latest: versions.slice(0).sort(semver.rcompare)[0] };
      return content;
    }

    return fetchMetadata(nameAndVersions.name)
      .flatMap(metadataContent => {
        const file = packageMetadataFilePath(nameAndVersions.name);
        const distributions = nameAndVersions.versions.map(version => ({
            name: nameAndVersions.name,
            version,
            dist: metadataContent.versions[version].dist,
            binary: metadataContent.versions[version].binary
          })
        )

        const distStream = Bacon.fromArray(distributions)
          .flatMap(distribution => {
            if (distribution.binary && distribution.binary.module_name) {
              return Bacon.fromArray(options.prebuiltBinaryProperties)
                .flatMapConcat(properties => {
                  if (prebuiltBinaryExists(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch)) {
                    console.log('Already downloaded pre-built binary ' + prebuiltBinaryFileName(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch));
                    return Bacon.later(0, distribution);
                  }
                  return fetchPrebuiltBinary(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch)
                    .doAction(data => {
                      fs.writeFileSync(prebuiltBinaryFilePath(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch), data);
                      console.log('Downloaded pre-built binary ' + prebuiltBinaryUrl(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch));
                    })
                    .mapError(() => {
                      console.log('Pre-built binary not available ' + prebuiltBinaryUrl(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch));
                      return distribution;
                    })
                    .map(distribution);
                }).skipDuplicates();
            } else {
              return Bacon.later(0, distribution);
            }
          })
          .flatMap(distribution => {
            if (tarballExists(distribution)) {
              if (distribution.binary) {
                const data = fs.readFileSync(packageTarballFilePath(distribution.name, distribution.version))
                metadataContent.versions[distribution.version].dist.shasum = sha1(data)
                metadataContent.versions[distribution.version].dist.integrity = sha512(data)
              }
              return Bacon.once('Already downloaded ' + distribution.name + '@' + distribution.version);
            }
            return fetchTarball(distribution.dist)
              .flatMap(data => {
                if (distribution.dist.integrity) {
                  if (sha512(data) != distribution.dist.integrity) {
                    return Bacon.later(0, new Bacon.Error('Integrity check with SHA512 failed for ' + distribution.name + '@' + distribution.version));
                  }
                } else if (sha1(data) != distribution.dist.shasum) {
                  return Bacon.later(0, new Bacon.Error('Integrity check with SHA1 failed for ' + distribution.name + '@' + distribution.version));
                }
                if (distribution.binary) {
                  return Bacon.fromNodeCallback(rewriteMetadataInTarball, distribution, data);
                }
                return Bacon.later(0, data);
              })
              .doAction(data => {
                metadataContent.versions[distribution.version].dist.shasum = sha1(data)
                metadataContent.versions[distribution.version].dist.integrity = sha512(data)
                fs.writeFileSync(packageTarballFilePath(distribution.name, distribution.version), data)
              })
              .map('Downloaded ' + distribution.name + '@' + distribution.version);
          });

          distStream.onEnd(() => {
            const content = JSON.stringify(cleanupMetadata(metadataContent, nameAndVersions.versions), null, options.pretty ? 2 : undefined);
            if (!fileExists(file) || content != fs.readFileSync(file)) {
              fs.writeFileSync(file, content);
            }
          })

          return distStream;
        });
  }

  const dependencies = dependenciesToArray(require(path.resolve(options.manifest)).dependencies);
  const downloaded = Bacon.fromArray(dependencies)
    .flatMap(resolveVersionAndDependencies)
    .mapEnd(collectedPackagesAsArray)
    .flatMap(Bacon.fromArray)
    .flatMapWithConcurrencyLimit(5, downloadPackage);

  downloaded.onEnd(() => {
    Bacon.fromEvent(readdirp({root: options.rootFolder, entryType: 'both'}), 'data')
      .filter((entry) => {
        return !requiredFiles[entry.fullPath];
      })
      .onValue((entry) => {
        if (entry.stat.isDirectory()) {
          fs.rmdir(entry.fullPath, (err) => {
            if (!err) {
              console.log(`Removed ${entry.path}/`);
            }
          });
        } else {
          fs.unlink(entry.fullPath, (err) => {
            if (!err) {
              console.log(`Removed ${entry.path}`);
            }
          });
        }
      });
  });

  downloaded.onError(err => {
    throw new Error(err);
  });
  downloaded.log();
}

module.exports = synchronize;
<commit_msg>Resolve also devDependencies dependency tree from root package.json<commit_after>const _ = require('lodash');
const Bacon = require('baconjs');
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const request = require('request');
const semver = require('semver');
const url = require('url');
const readdirp = require('readdirp');
const rimraf = require('rimraf');
const ssri = require('ssri');
const streamifier = require('streamifier');
const tar = require('tar-fs');
const zlib = require('zlib');

const responseCache = {};
const requiredFiles = {};

function synchronize(options) {

  function sha1(data) {
    const integrity = ssri.fromData(data, {algorithms: ['sha1']});
    return integrity.sha1[0].hexDigest();
  }

  function sha512(data) {
    const integrity = ssri.fromData(data, {algorithms: ['sha512']});
    return integrity.sha512[0].toString();
  }

  function fetchUrl(url, bodyIsBinary) {
    if (responseCache[url]) {
      return Bacon.later(0, responseCache[url]);
    }

    return Bacon.fromNodeCallback(callback => {
      request(url, {timeout: 60000, encoding: bodyIsBinary ? null : undefined},
        (error, response, body) => {
          if (!error && response.statusCode == 200) {
            if (!url.endsWith('.tgz')) {
              responseCache[url] = body;
            }
            callback(null, body);
          } else {
            const statusCode = response ? response.statusCode : 'n/a';
            callback(`Failed to fetch ${url} because of error '${error}' and/or HTTP status ${statusCode}`);
          }
        }
      );
    });
  }

  function encodeScopedPackage(name) {
    return name.replace("\/", "%2f");
  }

  function fetchMetadata(name) {
    return fetchUrl(url.resolve(options.registryUrl, encodeScopedPackage(name))).map(JSON.parse);
  }

  function fetchTarball(dist) {
    return fetchUrl(dist.tarball, true);
  }

  function fetchPrebuiltBinary(name, version, binaryMetadata, abi, platform, arch) {
    return fetchUrl(prebuiltBinaryUrl(name, version, binaryMetadata, abi, platform, arch), true);
  }

  function dependenciesToArray(dependencies) {
    return _(dependencies || {}).keys().map(key => {
      const versionRange = dependencies[key]
      if (versionRange instanceof Array) {
        return versionRange.map(version => {
          return {name: key, versionRange: version};
        });
      }
      return {name: key, versionRange: versionRange};
    }).flatten().value();
  }

  function packageFilename(name, version) {
    const normalized = name.replace('\/', '-');
    return `${normalized}-${version}.tgz`;
  }

  function packageMetadataFilePath(name) {
    mkdirp.sync(path.resolve(options.rootFolder, name));
    return path.resolve(options.rootFolder, path.join(name, 'index.json'));
  }

  function packageTarballFilePath(name, version) {
    return path.resolve(options.rootFolder, path.join(name, packageFilename(name, version)));
  }

  function packageTarballFileUrl(name, version) {
    return url.resolve(options.localUrl, name + '/' + packageFilename(name, version));
  }

  const collectedPackages = {};

  function collectPackage(package) {
    const versions = collectedPackages[package.name] || [];
    if (versions.indexOf(package.version) != -1) {
      return false;
    }
    versions.push(package.version);
    collectedPackages[package.name] = versions;
    return true;
  }

  function collectedPackagesAsArray() {
    return Object.keys(collectedPackages).map(name => ({
        name,
        versions: collectedPackages[name]
      }));
  }

  function resolveVersionAndDependencies(package) {
    return fetchMetadata(package.name)
      .map(metadata => {
          const available = Object.keys(metadata.versions);
          const version = semver.maxSatisfying(available, package.versionRange);
          return {
            name: package.name,
            version: version,
            dependencies: dependenciesToArray(metadata.versions[version].dependencies)
          };
        }
      )
      .flatMap(packageAndDependencies => {
        if (collectPackage(packageAndDependencies)) {
          return Bacon.fromArray(packageAndDependencies.dependencies).flatMapConcat(resolveVersionAndDependencies);
        }
        return Bacon.never();
      });
  }

  function fileExists(file) {
    requiredFiles[file] = true;
    return fs.existsSync(file);
  }

  function tarballExists(distribution) {
    return fileExists(packageTarballFilePath(distribution.name, distribution.version));
  }

  // see node-pre-gyp: /lib/util/versioning.js for documentation of possible values
  function formatPrebuilt(formatString, name, version, moduleName, abi, platform, arch) {
    const moduleVersion = semver.parse(version);
    const prerelease = (moduleVersion.prerelease || []).join('.');
    const build = (moduleVersion.build || []).join('.');

    return formatString
      .replace('{name}', name)
      .replace('{version}', version)
      .replace('{major}', moduleVersion.major)
      .replace('{minor}', moduleVersion.minor)
      .replace('{patch}', moduleVersion.patch)
      .replace('{prerelease}', prerelease)
      .replace('{build}', build)
      .replace('{module_name}', moduleName)
      .replace('{node_abi}', 'node-v' + abi)
      .replace('{platform}', platform)
      .replace('{arch}', arch)
      .replace('{configuration}', 'Release')
      .replace('{toolset}', '');
  }

  function prebuiltBinaryExists(name, version, binaryMetadata, abi, platform, arch) {
    return fileExists(prebuiltBinaryFilePath(name, version, binaryMetadata, abi, platform, arch));
  }

  function prebuiltBinaryFilePath(name, version, binaryMetadata, abi, platform, arch) {
    return path.resolve(options.rootFolder, path.join(name, prebuiltBinaryFileName(name, version, binaryMetadata, abi, platform, arch)));
  }

  function prebuiltBinaryRemotePath(name, version, binaryMetadata, abi, platform, arch) {
    return formatPrebuilt(binaryMetadata.remote_path, name, version, binaryMetadata.module_name, abi, platform, arch).replace(/[\/]+/g, '/');
  }

  function prebuiltBinaryFileName(name, version, binaryMetadata, abi, platform, arch) {
    return formatPrebuilt(binaryMetadata.package_name, name, version, binaryMetadata.module_name, abi, platform, arch).replace(/[\/]+/g, '/');
  }

  function prebuiltBinaryUrl(name, version, binaryMetadata, abi, platform, arch) {
    const remotePath = prebuiltBinaryRemotePath(name, version, binaryMetadata, abi, platform, arch);
    const fileName = prebuiltBinaryFileName(name, version, binaryMetadata, abi, platform, arch);
    return url.resolve(binaryMetadata.host, remotePath + fileName);
  }

  function rewriteMetadataInTarball(distribution, data, callback) {
    const extractDir = path.resolve(options.rootFolder, `_tmp_${distribution.name}`);
    mkdirp.sync(extractDir);
    const tgzIn = streamifier
      .createReadStream(data)
      .pipe(zlib.createGunzip())
      .pipe(tar.extract(extractDir));

    tgzIn.on('finish', () => {
      const packageJsonPath = path.resolve(extractDir, path.join('package', 'package.json'));
      const packagedMetadata = require(packageJsonPath);
      packagedMetadata.binary.host = options.localUrl;
      packagedMetadata.binary.remote_path = `/${distribution.name}/`;
      fs.writeFileSync(packageJsonPath, JSON.stringify(packagedMetadata, null, 2));

      const chunks = [];
      const tgzOut = tar.pack(extractDir).pipe(zlib.createGzip());
      tgzOut.on('data', chunk => chunks.push(chunk));
      tgzOut.on('end', () => {
        rimraf(extractDir, err => {
          callback(err, Buffer.concat(chunks));
        })
      })
      tgzOut.on('error', callback);
    })
    tgzIn.on('error', callback);
  }

  function downloadPackage(nameAndVersions) {
    function cleanupMetadata(metadataContent, versions) {
      const content = _.cloneDeep(metadataContent);
      Object.keys(content.versions).forEach(version => {
        if (versions.indexOf(version) == -1) {
          delete content.versions[version];
          delete content.time[version];
        }
        else {
          content.versions[version].dist.tarball = packageTarballFileUrl(nameAndVersions.name, version);
          if (content.versions[version].binary) {
            content.versions[version].binary.host = options.localUrl;
            content.versions[version].binary.remote_path = `/${nameAndVersions.name}/`;
          }
        }
      });
      content['dist-tags'] = { latest: versions.slice(0).sort(semver.rcompare)[0] };
      return content;
    }

    return fetchMetadata(nameAndVersions.name)
      .flatMap(metadataContent => {
        const file = packageMetadataFilePath(nameAndVersions.name);
        const distributions = nameAndVersions.versions.map(version => ({
            name: nameAndVersions.name,
            version,
            dist: metadataContent.versions[version].dist,
            binary: metadataContent.versions[version].binary
          })
        )

        const distStream = Bacon.fromArray(distributions)
          .flatMap(distribution => {
            if (distribution.binary && distribution.binary.module_name) {
              return Bacon.fromArray(options.prebuiltBinaryProperties)
                .flatMapConcat(properties => {
                  if (prebuiltBinaryExists(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch)) {
                    console.log('Already downloaded pre-built binary ' + prebuiltBinaryFileName(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch));
                    return Bacon.later(0, distribution);
                  }
                  return fetchPrebuiltBinary(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch)
                    .doAction(data => {
                      fs.writeFileSync(prebuiltBinaryFilePath(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch), data);
                      console.log('Downloaded pre-built binary ' + prebuiltBinaryUrl(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch));
                    })
                    .mapError(() => {
                      console.log('Pre-built binary not available ' + prebuiltBinaryUrl(distribution.name, distribution.version, distribution.binary, properties.abi, properties.platform, properties.arch));
                      return distribution;
                    })
                    .map(distribution);
                }).skipDuplicates();
            } else {
              return Bacon.later(0, distribution);
            }
          })
          .flatMap(distribution => {
            if (tarballExists(distribution)) {
              if (distribution.binary) {
                const data = fs.readFileSync(packageTarballFilePath(distribution.name, distribution.version))
                metadataContent.versions[distribution.version].dist.shasum = sha1(data)
                metadataContent.versions[distribution.version].dist.integrity = sha512(data)
              }
              return Bacon.once('Already downloaded ' + distribution.name + '@' + distribution.version);
            }
            return fetchTarball(distribution.dist)
              .flatMap(data => {
                if (distribution.dist.integrity) {
                  if (sha512(data) != distribution.dist.integrity) {
                    return Bacon.later(0, new Bacon.Error('Integrity check with SHA512 failed for ' + distribution.name + '@' + distribution.version));
                  }
                } else if (sha1(data) != distribution.dist.shasum) {
                  return Bacon.later(0, new Bacon.Error('Integrity check with SHA1 failed for ' + distribution.name + '@' + distribution.version));
                }
                if (distribution.binary) {
                  return Bacon.fromNodeCallback(rewriteMetadataInTarball, distribution, data);
                }
                return Bacon.later(0, data);
              })
              .doAction(data => {
                metadataContent.versions[distribution.version].dist.shasum = sha1(data)
                metadataContent.versions[distribution.version].dist.integrity = sha512(data)
                fs.writeFileSync(packageTarballFilePath(distribution.name, distribution.version), data)
              })
              .map('Downloaded ' + distribution.name + '@' + distribution.version);
          });

          distStream.onEnd(() => {
            const content = JSON.stringify(cleanupMetadata(metadataContent, nameAndVersions.versions), null, options.pretty ? 2 : undefined);
            if (!fileExists(file) || content != fs.readFileSync(file)) {
              fs.writeFileSync(file, content);
            }
          })

          return distStream;
        });
  }

  const manifest = require(path.resolve(options.manifest))
  const dependencies = dependenciesToArray(_.merge(manifest.dependencies, manifest.devDependencies));
  const downloaded = Bacon.fromArray(dependencies)
    .flatMap(resolveVersionAndDependencies)
    .mapEnd(collectedPackagesAsArray)
    .flatMap(Bacon.fromArray)
    .flatMapWithConcurrencyLimit(5, downloadPackage);

  downloaded.onEnd(() => {
    Bacon.fromEvent(readdirp({root: options.rootFolder, entryType: 'both'}), 'data')
      .filter((entry) => {
        return !requiredFiles[entry.fullPath];
      })
      .onValue((entry) => {
        if (entry.stat.isDirectory()) {
          fs.rmdir(entry.fullPath, (err) => {
            if (!err) {
              console.log(`Removed ${entry.path}/`);
            }
          });
        } else {
          fs.unlink(entry.fullPath, (err) => {
            if (!err) {
              console.log(`Removed ${entry.path}`);
            }
          });
        }
      });
  });

  downloaded.onError(err => {
    throw new Error(err);
  });
  downloaded.log();
}

module.exports = synchronize;
<|endoftext|>


<commit_before>'use babel';

import path from 'path';
import React from 'react';
import ReactDOM from 'react-dom';
import {CompositeDisposable} from 'atom';
import {delimiter} from 'path';
import Dispatcher from './dispatcher';
import NotebookEditor from './notebook-editor';
import NotebookEditorView from './notebook-editor-view';``

export default {

  activate(state) {
    // console.log('Activated');
    fixPath();
    this.openerDisposable = atom.workspace.addOpener(openURI);
    this.commands = atom.commands.add('.notebook-cell atom-text-editor', 'jupyter-notebook-atom:run', this.run);
    atom.views.addViewProvider({
      modelConstructor: NotebookEditor,
      createView: model => {
        let el = document.createElement('div');
        el.classList.add('notebook-wrapper');
        let viewComponent = ReactDOM.render(
          <NotebookEditorView store={model} />,
          el);
        return el;
      }
    });
  },

  deactivate() {
    Dispatcher.dispatch({
      actionType: Dispatcher.actions.destroy
    });
    this.openerDisposable.dispose();
    this.commands.dispose();
  },

  toggle() {
    console.log('JupyterNotebookAtom was toggled!');
    if (this.modalPanel.isVisible()) {
      return this.modalPanel.hide();
    } else {
      return this.modalPanel.show();
    }
  },

  run() {
    // console.log('Run cell');
    Dispatcher.dispatch({
      actionType: Dispatcher.actions.run_active_cell
      // cellID: this.props.data.getIn(['metadata', 'id'])
    });
  }

};

function fixPath() {
  if (process.platform === 'darwin') {
    process.env.PATH = process.env.PATH.split(delimiter).reduce((result, path) => {
      if (!result.find(item => item === path)) result.push(path);
      return result;
    }, [
      '/usr/local/bin',
      '/usr/bin',
      '/bin',
      '/usr/local/sbin',
      '/usr/sbin',
      '/sbin',
      './node_modules/.bin'
    ]).join(delimiter);
  }
}

function openURI(uriToOpen) {
  const notebookExtensions = ['.ipynb'];
  let uriExtension = path.extname(uriToOpen).toLowerCase();
  if (notebookExtensions.find(extension => extension === uriExtension)) return new NotebookEditor(uriToOpen);
}
<commit_msg>Add config for jupyter binary path<commit_after>'use babel';

import path from 'path';
import React from 'react';
import ReactDOM from 'react-dom';
import {CompositeDisposable} from 'atom';
import {delimiter} from 'path';
import Dispatcher from './dispatcher';
import NotebookEditor from './notebook-editor';
import NotebookEditorView from './notebook-editor-view';

export default {

  config: {
    jupyterPath: {
      title: 'Path to jupyter binary',
      description: '',
      type: 'string',
      default: 'usr/local/bin'
    }
  },

  activate(state) {
    // console.log('Activated');
    fixPath();
    this.openerDisposable = atom.workspace.addOpener(openURI);
    this.commands = atom.commands.add('.notebook-cell atom-text-editor', 'jupyter-notebook-atom:run', this.run);
    atom.views.addViewProvider({
      modelConstructor: NotebookEditor,
      createView: model => {
        let el = document.createElement('div');
        el.classList.add('notebook-wrapper');
        let viewComponent = ReactDOM.render(
          <NotebookEditorView store={model} />,
          el);
        return el;
      }
    });
  },

  deactivate() {
    Dispatcher.dispatch({
      actionType: Dispatcher.actions.destroy
    });
    this.openerDisposable.dispose();
    this.commands.dispose();
  },

  toggle() {
    console.log('JupyterNotebookAtom was toggled!');
    if (this.modalPanel.isVisible()) {
      return this.modalPanel.hide();
    } else {
      return this.modalPanel.show();
    }
  },

  run() {
    // console.log('Run cell');
    Dispatcher.dispatch({
      actionType: Dispatcher.actions.run_active_cell
      // cellID: this.props.data.getIn(['metadata', 'id'])
    });
  }

};

function fixPath() {
  let defaultPaths = [
    '/usr/local/bin',
    '/usr/bin',
    '/bin',
    '/usr/local/sbin',
    '/usr/sbin',
    '/sbin',
    './node_modules/.bin'
  ];
  let jupyterPath = atom.config.get('jupyter-notebook.jupyterPath');
  if (defaultPaths.indexOf(jupyterPath) < 0) defaultPaths.unshift(jupyterPath);
  if (process.platform === 'darwin') {
    process.env.PATH = process.env.PATH.split(delimiter).reduce((result, path) => {
      if (!result.find(item => item === path)) result.push(path);
      return result;
    }, defaultPaths).join(delimiter);
  }
}

function openURI(uriToOpen) {
  const notebookExtensions = ['.ipynb'];
  let uriExtension = path.extname(uriToOpen).toLowerCase();
  if (notebookExtensions.find(extension => extension === uriExtension)) return new NotebookEditor(uriToOpen);
}
<|endoftext|>


<commit_before>// vim: set ts=2 sw=2:
"use strict";

let preloader = document.querySelector('.preloader');
let preloaderText = preloader.querySelector(':scope .preloader__text');
let the_physics = new Physics();
let the_longcat = new LongCat(document.querySelector('.longcat'), the_physics);
let sections = setupSections();

function setupSections() {
  const allSections = document.querySelectorAll('.section');
  let sections = [];

  sections[0] = new WallSection(allSections[0], the_physics);
  sections[1] = new EarthSection(allSections[1], the_physics);
  sections[2] = new Section(allSections[2], the_physics);  // street
  sections[3] = new BuildingsSection(allSections[3], the_physics);
  sections[4] = new Section(allSections[4], the_physics); // clouds
  sections[5] = new Section(allSections[5], the_physics); // aerspace one
  sections[6] = new InfiniteStartSection(allSections[6], the_physics); // infinite start

  return sections;
}

window.requestAnimationFrame = window.requestAnimationFrame
                            || window.mozRequestAnimationFrame
                            || window.webkitRequestAnimationFrame
                            || window.msRequestAnimationFrame
                            || function(cb) { window.setTimeout(cb, 1000/60); };

const score_el = document.querySelector('.score__number');
const highs_el = document.querySelector('.score__number--high');

// Rather use window.performance if it's available.
// It has sub-millisecond precision, while Date.now only has milliseconds.
const rtc = window.performance || Date;
let t0 = rtc.now();
let high_score = 0;

function mainloop() {
  let t = rtc.now();
  let dt = 0.001*(t - t0);  // from milliseconds to seconds

  // When the tab gets hidden, this animate loop is not called, which is good
  // as it pauses the game. But when the tab gets visible again, the dt is huge:
  // as large as the tab was hidden. We don't want this, as it undoes the pause.
  // so we only update on small dt.
  if (dt < 0.5) {
    dt = the_physics.update(dt);
    the_longcat.update(dt);

    sections.forEach(section => {
      section.update(dt);
    });

    // Update the titlebar according to the score.
    document.title = `Longcat is lo${'o'.repeat(the_physics.score)}ng!`

    // Update the score itself and the high-score.
    high_score = Math.max(high_score, the_physics.score);
    score_el.innerHTML = the_physics.score.toFixed(1);
    highs_el.innerHTML = high_score.toFixed(1);
  }

  // For the next timestep.
  t0 = t;
  window.requestAnimationFrame(mainloop);
}
window.requestAnimationFrame(mainloop);

function raise_longcat(ev) {
  // remove preloader
  if(preloader) {
    document.body.removeChild(preloader);
    preloader = false;
  }
  // Avoid stuff like drag/drop selection etc.
  ev.preventDefault();

  // Meow when we get starteeeed!
  if (the_physics.score == 0) {
      the_longcat.meow();
  }

  the_physics.speedup();
}

function init() {
  // preloader to instructions
  preloader.classList.add('preloader--loaded');
  preloaderText.classList.add('preloader__text--loaded');
  preloaderText.innerHTML = 'click or tab as fast as you can !!!!';

  // A click raises the longcat!
  window.addEventListener('mousedown', raise_longcat);
  window.addEventListener('touchstart', raise_longcat);

  // Tweet score.
  document.querySelector('.tweet').addEventListener('click', ev => {
    ev.preventDefault();
    window.open(
      `http://twitter.com/intent/tweet?text=Beat my highscore of ${high_score.toFixed(2)}km` +
      `&url=${encodeURIComponent("http://longcat.wtf")}` +
      `&hashtags=longcat,longcatwtf`,
      '_blank'
    );
  });
}

// Some cheats for testing
window.addEventListener('keyup', (ev) => {
  ev.preventDefault();
  if (ev.key == 'ArrowUp' || ev.key == 'Up') {
    the_physics.g = 0;
    the_physics.vy += 0.01;
  } else if (ev.key == 'ArrowDown' || ev.key == 'Down') {
    the_physics.g = 0;
    the_physics.vy -= 0.01;
  } else if (ev.key == '+' || ev.key == 'Add') {
    the_physics.vy = 1;
  } else if (ev.key == ' ') {
    the_physics.g = 0;
    the_physics.vy = 0;
    the_physics.a = 0;
  }
  console.log(ev.key)
})

window.addEventListener('keydown', (ev) => { ev.preventDefault(); });

window.onload = function() {
  init();
};
<commit_msg>fix typo<commit_after>// vim: set ts=2 sw=2:
"use strict";

let preloader = document.querySelector('.preloader');
let preloaderText = preloader.querySelector(':scope .preloader__text');
let the_physics = new Physics();
let the_longcat = new LongCat(document.querySelector('.longcat'), the_physics);
let sections = setupSections();

function setupSections() {
  const allSections = document.querySelectorAll('.section');
  let sections = [];

  sections[0] = new WallSection(allSections[0], the_physics);
  sections[1] = new EarthSection(allSections[1], the_physics);
  sections[2] = new Section(allSections[2], the_physics);  // street
  sections[3] = new BuildingsSection(allSections[3], the_physics);
  sections[4] = new Section(allSections[4], the_physics); // clouds
  sections[5] = new Section(allSections[5], the_physics); // aerspace one
  sections[6] = new InfiniteStartSection(allSections[6], the_physics); // infinite start

  return sections;
}

window.requestAnimationFrame = window.requestAnimationFrame
                            || window.mozRequestAnimationFrame
                            || window.webkitRequestAnimationFrame
                            || window.msRequestAnimationFrame
                            || function(cb) { window.setTimeout(cb, 1000/60); };

const score_el = document.querySelector('.score__number');
const highs_el = document.querySelector('.score__number--high');

// Rather use window.performance if it's available.
// It has sub-millisecond precision, while Date.now only has milliseconds.
const rtc = window.performance || Date;
let t0 = rtc.now();
let high_score = 0;

function mainloop() {
  let t = rtc.now();
  let dt = 0.001*(t - t0);  // from milliseconds to seconds

  // When the tab gets hidden, this animate loop is not called, which is good
  // as it pauses the game. But when the tab gets visible again, the dt is huge:
  // as large as the tab was hidden. We don't want this, as it undoes the pause.
  // so we only update on small dt.
  if (dt < 0.5) {
    dt = the_physics.update(dt);
    the_longcat.update(dt);

    sections.forEach(section => {
      section.update(dt);
    });

    // Update the titlebar according to the score.
    document.title = `Longcat is lo${'o'.repeat(the_physics.score)}ng!`

    // Update the score itself and the high-score.
    high_score = Math.max(high_score, the_physics.score);
    score_el.innerHTML = the_physics.score.toFixed(1);
    highs_el.innerHTML = high_score.toFixed(1);
  }

  // For the next timestep.
  t0 = t;
  window.requestAnimationFrame(mainloop);
}
window.requestAnimationFrame(mainloop);

function raise_longcat(ev) {
  // remove preloader
  if(preloader) {
    document.body.removeChild(preloader);
    preloader = false;
  }
  // Avoid stuff like drag/drop selection etc.
  ev.preventDefault();

  // Meow when we get starteeeed!
  if (the_physics.score == 0) {
      the_longcat.meow();
  }

  the_physics.speedup();
}

function init() {
  // preloader to instructions
  preloader.classList.add('preloader--loaded');
  preloaderText.classList.add('preloader__text--loaded');
  preloaderText.innerHTML = 'click or tap as fast as you can !!!!';

  // A click raises the longcat!
  window.addEventListener('mousedown', raise_longcat);
  window.addEventListener('touchstart', raise_longcat);

  // Tweet score.
  document.querySelector('.tweet').addEventListener('click', ev => {
    ev.preventDefault();
    window.open(
      `http://twitter.com/intent/tweet?text=Beat my highscore of ${high_score.toFixed(2)}km` +
      `&url=${encodeURIComponent("http://longcat.wtf")}` +
      `&hashtags=longcat,longcatwtf`,
      '_blank'
    );
  });
}

// Some cheats for testing
window.addEventListener('keyup', (ev) => {
  ev.preventDefault();
  if (ev.key == 'ArrowUp' || ev.key == 'Up') {
    the_physics.g = 0;
    the_physics.vy += 0.01;
  } else if (ev.key == 'ArrowDown' || ev.key == 'Down') {
    the_physics.g = 0;
    the_physics.vy -= 0.01;
  } else if (ev.key == '+' || ev.key == 'Add') {
    the_physics.vy = 1;
  } else if (ev.key == ' ') {
    the_physics.g = 0;
    the_physics.vy = 0;
    the_physics.a = 0;
  }
  console.log(ev.key)
})

window.addEventListener('keydown', (ev) => { ev.preventDefault(); });

window.onload = function() {
  init();
};
<|endoftext|>


<commit_before>const themes = require('../themes')
const tools = require('../tools')

const _ = require('lodash')
const chalk = require('chalk')
const needle = require('needle')
const noon = require('noon')

const CFILE = `${process.env.HOME}/.leximaven.noon`

exports.command = 'dmuse <condition>'
exports.desc = 'Datamuse query'
exports.builder = {
  out: {
    alias: 'o',
    desc: 'Write cson, json, noon, plist, yaml, xml',
    default: '',
    type: 'string',
  },
  force: {
    alias: 'f',
    desc: 'Force overwriting outfile',
    default: false,
    type: 'boolean',
  },
  save: {
    alias: 's',
    desc: 'Save flags to config file',
    default: false,
    type: 'boolean',
  },
  max: {
    alias: 'm',
    desc: 'Maximum number of results, 1 to 1000',
    default: 5,
    type: 'number',
  },
}
exports.handler = (argv) => {
  tools.checkConfig(CFILE)
  let config = noon.load(CFILE)
  const userConfig = {
    dmuse: {
      max: argv.m,
    },
  }
  if (config.merge) config = _.merge({}, config, userConfig)
  const theme = themes.loadTheme(config.theme)
  if (config.verbose) themes.labelDown('Datamuse', theme, null)
  const ccont = []
  if (argv._.length > 1) {
    for (let i = 0; i <= argv._.length - 1; i++) {
      ccont.push(argv._[i])
    }
  }
  const prefix = 'http://api.datamuse.com/words?'
  let conditions = `max=${config.dmuse.max}&`
  _.each(ccont, (value) => {
    conditions = `${conditions}&${value}`
  })
  let url = `${prefix}${conditions}`
  url = encodeURI(url)
  const tags = {
    n: 'noun',
    adj: 'adjective',
    adv: 'adverb',
    syn: 'synonym',
  }
  const tofile = { type: 'datamuse', source: 'http://datamuse.com/api' }
  const ctstyle = _.get(chalk, theme.content.style)
  needle.get(url, (error, response) => {
    if (!error && response.statusCode === 200) {
      const resp = response.body
      for (let i = 0; i <= resp.length - 1; i++) {
        const item = resp[i]
        themes.labelRight('Match', theme, `${item.word} `)
        tofile[[`match${i}`]] = item.word
        if (item.tags !== undefined && item.tags !== []) {
          themes.labelRight('Tags', theme, null)
          for (let j = 0; j <= item.tags.length - 1; j++) {
            if (j === item.tags.length - 1) {
              process.stdout.write(ctstyle(`${tags[item.tags[j]]}`))
              tofile[[`tags${j}`]] = tags[item.tags[j]]
            } else {
              process.stdout.write(ctstyle(`${tags[item.tags[j]]}, `))
            }
          }
          console.log('')
        }
      }
      if (argv.o) tools.outFile(argv.o, argv.f, tofile)
      if (argv.s && config.merge) noon.save(CFILE, config)
      if (argv.s && !config.merge) console.err(chalk.red('Set option merge to true!'))
    } else {
      console.error(`${chalk.red.bold(`HTTP ${response.statusCode}:`)} ${chalk.red(error)}`)
    }
  })
}
<commit_msg>fix(datamuse): fix arg parsing<commit_after>const themes = require('../themes')
const tools = require('../tools')

const _ = require('lodash')
const chalk = require('chalk')
const needle = require('needle')
const noon = require('noon')

const CFILE = `${process.env.HOME}/.leximaven.noon`

exports.command = 'dmuse <condition>'
exports.desc = 'Datamuse query'
exports.builder = {
  out: {
    alias: 'o',
    desc: 'Write cson, json, noon, plist, yaml, xml',
    default: '',
    type: 'string',
  },
  force: {
    alias: 'f',
    desc: 'Force overwriting outfile',
    default: false,
    type: 'boolean',
  },
  save: {
    alias: 's',
    desc: 'Save flags to config file',
    default: false,
    type: 'boolean',
  },
  max: {
    alias: 'm',
    desc: 'Maximum number of results, 1 to 1000',
    default: 5,
    type: 'number',
  },
}
exports.handler = (argv) => {
  tools.checkConfig(CFILE)
  let config = noon.load(CFILE)
  const userConfig = {
    dmuse: {
      max: argv.m,
    },
  }
  if (config.merge) config = _.merge({}, config, userConfig)
  const theme = themes.loadTheme(config.theme)
  if (config.verbose) themes.labelDown('Datamuse', theme, null)
  const ccont = []
  ccont.push(argv.condition)
  if (argv._.length > 1) {
    for (let i = 1; i <= argv._.length - 1; i++) {
      ccont.push(argv._[i])
    }
  }
  const prefix = 'http://api.datamuse.com/words?'
  let conditions = `max=${config.dmuse.max}&`
  _.each(ccont, (value) => {
    conditions = `${conditions}&${value}`
  })
  let url = `${prefix}${conditions}`
  url = encodeURI(url)
  const tags = {
    n: 'noun',
    adj: 'adjective',
    adv: 'adverb',
    syn: 'synonym',
  }
  const tofile = { type: 'datamuse', source: 'http://datamuse.com/api' }
  const ctstyle = _.get(chalk, theme.content.style)
  needle.get(url, (error, response) => {
    if (!error && response.statusCode === 200) {
      const resp = response.body
      for (let i = 0; i <= resp.length - 1; i++) {
        const item = resp[i]
        themes.labelRight('Match', theme, `${item.word} `)
        tofile[[`match${i}`]] = item.word
        if (item.tags !== undefined && item.tags !== []) {
          themes.labelRight('Tags', theme, null)
          for (let j = 0; j <= item.tags.length - 1; j++) {
            if (j === item.tags.length - 1) {
              process.stdout.write(ctstyle(`${tags[item.tags[j]]}`))
              tofile[[`tags${j}`]] = tags[item.tags[j]]
            } else {
              process.stdout.write(ctstyle(`${tags[item.tags[j]]}, `))
            }
          }
          console.log('')
        }
      }
      if (argv.o) tools.outFile(argv.o, argv.f, tofile)
      if (argv.s && config.merge) noon.save(CFILE, config)
      if (argv.s && !config.merge) console.err(chalk.red('Set option merge to true!'))
    } else {
      console.error(`${chalk.red.bold(`HTTP ${response.statusCode}:`)} ${chalk.red(error)}`)
    }
  })
}
<|endoftext|>


<commit_before>'use babel';

export default {
  config: {
    ansibleLintExecutablePath: {
      title: 'Ansible-Lint Executable Path',
      type: 'string',
      description: 'Path to Ansible-Lint executable (e.g. /usr/bin/ansible-lint) if not in shell env path.',
      default: 'ansible-lint',
    },
    rulesDirDefault: {
      title: 'Additionally use the default rules directories with Ansible-Lint (only if using non-default rules directories).',
      type: 'boolean',
      default: false,
    },
    rulesDirs: {
      title: 'Rules Directories',
      type: 'array',
      description: 'Non-default rules directories to use with Ansible-Lint.',
      default: [''],
      items: {
        type: 'string'
      }
    },
    excludeDirs: {
      title: 'Exclude Directories',
      type: 'array',
      description: 'Absolute path directories to exclude during linting.',
      default: [''],
      items: {
        type: 'string'
      }
    },
    useProjectConfig: {
      title: 'Use project Ansible-Lint config file.',
      type: 'boolean',
      description: 'Use an ansible-lint configuration file named `.ansible-lint` in the root level of the project directory. Overrides other settings besides executable path and blacklist.',
      default: false,
    },
    blacklist: {
      title: 'Exclude Regexp for .yml',
      type: 'string',
      description: 'Regular expression for .yml filenames to ignore (e.g. travis|docker would ignore docker-compose.yml and .travis.yml).',
      default: '',
    },
    ruleSkips: {
      title: 'Tag/Rule Skips',
      type: 'string',
      description: 'List of comma-delimited tags and/or rules to skip when performing checks.',
      default: '',
    },
    timeout: {
      title: 'Linting Timeout',
      type: 'number',
      description: 'Number of seconds to wait on lint attempt before timing out.',
      default: 10,
    },
    displaySeverity: {
      title: 'Display Severity',
      type: 'boolean',
      description: 'Display the severity of the warning inside the message.',
      default: false,
    },
    lintProject: {
      title: 'Lint Project',
      type: 'boolean',
      description: 'Lint entire project if open file is not a playbook (useful if your projects are playbook collections, roles, or role collections)',
      default: false,
    }
  },

  // activate linter
  activate() {
    const helpers = require('atom-linter');

    // check ansible-lint version
    helpers.exec(atom.config.get('linter-ansible-linting.ansibleLintExecutablePath'), ['-T']).then(output => {
      // check if ansible-lint >= 5
      if (!(/no-loop-var-prefix/.exec(output))) {
        atom.notifications.addWarning(
          'ansible-lint < 5.0 is unsupported. Backwards compatibility should exist, but is not guaranteed.',
          {
            detail: "Please upgrade your version of ansible-lint to >= 5.0.\n",
            dismissable: true
          }
        );
      }
    });
  },

  deactivate() {
    this.idleCallbacks.forEach((callbackID) => window.cancelIdleCallback(callbackID));
    this.idleCallbacks.clear();
    this.subscriptions.dispose();
  },

  provideLinter() {
    return {
      name: 'Ansible-Lint',
      grammarScopes: ['source.ansible', 'source.ansible-advanced'],
      scope: 'project',
      lintsOnChange: false,
      lint: async (textEditor) => {
        // setup variables
        const helpers = require('atom-linter');
        const file = textEditor.getPath();
        const dir = require('path').dirname(file);

        // bail out if this is on the blacklist
        if (atom.config.get('linter-ansible-linting.blacklist') !== '') {
          blacklist = new RegExp(atom.config.get('linter-ansible-linting.blacklist'))
          if (blacklist.exec(file))
            return [];
        }

        // cannot cwd in project path and then add file relative path to args because ansible relies on pathing relative to directory execution for includes
        var projectPath = atom.project.relativizePath(dir)[0];

        // pep8 output and no color
        var args = ['-p', '--nocolor', '--offline'];

        // add parseable severity flag if requested
        if (atom.config.get('linter-ansible-linting.displaySeverity'))
          args.push('--parseable-severity');

        // use config file if specified
        if (atom.config.get('linter-ansible-linting.useProjectConfig')) {
          // use ansible-lint config file in root project level
          args.push(...['-c', projectPath + '/.ansible-lint'])
        }
        else {
          // skip checks that user has opted to skip
          if (atom.config.get('linter-ansible-linting.ruleSkips') != '')
            args.push(...['-x', atom.config.get('linter-ansible-linting.ruleSkips')])

          // add additional rules directories
          if (atom.config.get('linter-ansible-linting.rulesDirs')[0] !== '') {
            if (atom.config.get('linter-ansible-linting.rulesDirDefault'))
              args.push('-R')

            for (i = 0; i < atom.config.get('linter-ansible-linting.rulesDirs').length; i++)
              args.push(...['-r', atom.config.get('linter-ansible-linting.rulesDirs')[i]]);
          }

          // exclude certain directories from checks
          if (atom.config.get('linter-ansible-linting.excludeDirs')[0] != '')
            for (i = 0; i < atom.config.get('linter-ansible-linting.excludeDirs').length; i++)
              args.push(...['--exclude', atom.config.get('linter-ansible-linting.excludeDirs')[i]]);
        }

        // lint file if playbook, else lint project
        if ((atom.config.get('linter-ansible-linting.lintProject')) && (!(/hosts:/.exec(textEditor.getText()))) && (!(projectPath == null)))
          args.push(projectPath);
        else
          args.push(file);

        // fix file not in a project; need that null coalescing operator
        projectPath = projectPath == null ? '' : projectPath + require('path').sep

        return helpers.exec(atom.config.get('linter-ansible-linting.ansibleLintExecutablePath'), args, {cwd: projectPath, stream: 'both', timeout: atom.config.get('linter-ansible-linting.timeout') * 1000}).then(output => {
          // initialize variable for linter return here for either linter output or errors
          var toReturn = [];

          output.stdout.split(/\r?\n/).forEach((line) => {
            // setup matchers
            const lintMatches = /(.*.ya?ml):(\d+):\s([a-z-]+)\s(.*)/.exec(line);
            const lintMatchesSyntax = /(.*.ya?ml):(\d+):(\d+):\s([a-z-]+)\s(.*)/.exec(line);
            const lintMatchesGeneric = /(\w+:\d+:.*)/.exec(line);
            const lintMatchesSyntaxUnknown = /Ansible syntax check failed/.exec(line);

            // check for ansible-lint 5 syntax issues with unknown info
            if (lintMatchesSyntaxUnknown != null) {
              toReturn.push({
                severity: 'error',
                excerpt: 'Ansible syntax check failed for unknown reason; use syntax checker for more information.',
                location: {
                  file: file,
                  position: [[0, 0], [0, 1]],
                },
              });
            }
            // check for ansible-lint 5 output linting issues
            else if (lintMatches != null) {
              toReturn.push({
                severity: 'warning',
                excerpt: lintMatches[3] + ': ' + lintMatches[4],
                location: {
                  // ansible-lint plus atom-linter combined now lose pathing info for this kind of parsing
                  // prepend dir to file
                  file: projectPath + lintMatches[1],
                  position: [[Number.parseInt(lintMatches[2]) - 1, 0], [Number.parseInt(lintMatches[2]) - 1, 1]],
                },
              });
            }
            // check for ansible-lint 5 syntax issues with info
            else if (lintMatchesSyntax != null) {
              toReturn.push({
                severity: 'error',
                excerpt: lintMatchesSyntax[4] + ': ' + lintMatchesSyntax[5],
                location: {
                  // ansible-lint plus atom-linter combined now lose pathing info for this kind of parsing
                  // prepend dir to file
                  file: projectPath + lintMatchesSyntax[1],
                  position: [[Number.parseInt(lintMatchesSyntax[2]) - 1, Number.parseInt(lintMatchesSyntax[3]) - 1], [Number.parseInt(lintMatchesSyntax[2]) - 1, Number.parseInt(lintMatchesSyntax[3])]],
                },
              });
            }
            // check for generic failure in version five (super greedy, so must be last conditional)
            else if (lintMatchesGeneric != null) {
              toReturn.push({
                severity: 'warning',
                excerpt: lintMatchesGeneric[1],
                location: {
                  file: file,
                  position: [[0, 0], [0, 1]],
                },
              });
            }
          });
          // parse stderr
          stderr = output.stderr
          const stdinMatches = /\.dirname/.exec(stderr);
          const extraOutputMatches = /skip specific rules/.exec(stderr);
          const warningMatches = /WARNING/.exec(stderr)

          // check for stdin lint attempt
          if (stdinMatches != null) {
            toReturn.push({
              severity: 'info',
              excerpt: 'Ansible-Lint cannot reliably lint on stdin due to nonexistent pathing on includes and roles. Please save this playbook to your filesystem.',
              location: {
                file: 'Save this playbook.',
                position: [[0, 0], [0, 1]],
              },
            });
          }
          // output other errors directly to Atom notification display, and ignore noisy output
          else if ((extraOutputMatches == null) && (warningMatches == null) && (stderr != '')) {
            atom.notifications.addError(
              'An unexpected error with ansible, ansible-lint, linter-ansible-linting, atom, linter, and/or your playbook/role has occurred.',
              { detail: stderr }
            );
          };
          return toReturn;
        });
      }
    };
  }
};
<commit_msg>code cleanup<commit_after>'use babel';

export default {
  config: {
    ansibleLintExecutablePath: {
      title: 'Ansible-Lint Executable Path',
      type: 'string',
      description: 'Path to Ansible-Lint executable (e.g. /usr/bin/ansible-lint) if not in shell env path.',
      default: 'ansible-lint',
    },
    rulesDirDefault: {
      title: 'Additionally use the default rules directories with Ansible-Lint (only if using non-default rules directories).',
      type: 'boolean',
      default: false,
    },
    rulesDirs: {
      title: 'Rules Directories',
      type: 'array',
      description: 'Non-default rules directories to use with Ansible-Lint.',
      default: [''],
      items: {
        type: 'string'
      },
    },
    excludeDirs: {
      title: 'Exclude Directories',
      type: 'array',
      description: 'Absolute path directories to exclude during linting.',
      default: [''],
      items: {
        type: 'string'
      },
    },
    useProjectConfig: {
      title: 'Use project Ansible-Lint config file.',
      type: 'boolean',
      description: 'Use an ansible-lint configuration file named `.ansible-lint` in the root level of the project directory. Overrides other settings besides executable path and blacklist.',
      default: false,
    },
    blacklist: {
      title: 'Exclude Regexp for .yml',
      type: 'string',
      description: 'Regular expression for .yml filenames to ignore (e.g. travis|docker would ignore docker-compose.yml and .travis.yml).',
      default: '',
    },
    ruleSkips: {
      title: 'Tag/Rule Skips',
      type: 'string',
      description: 'List of comma-delimited tags and/or rules to skip when performing checks.',
      default: '',
    },
    timeout: {
      title: 'Linting Timeout',
      type: 'number',
      description: 'Number of seconds to wait on lint attempt before timing out.',
      default: 10,
    },
    displaySeverity: {
      title: 'Display Severity',
      type: 'boolean',
      description: 'Display the severity of the warning inside the message.',
      default: false,
    },
    lintProject: {
      title: 'Lint Project',
      type: 'boolean',
      description: 'Lint entire project if open file is not a playbook (useful if your projects are playbook collections, roles, or role collections)',
      default: false,
    }
  },

  // activate linter
  activate() {
    const helpers = require('atom-linter');

    // check ansible-lint version
    helpers.exec(atom.config.get('linter-ansible-linting.ansibleLintExecutablePath'), ['-T']).then(output => {
      // check if ansible-lint >= 5
      if (!(/no-loop-var-prefix/.exec(output))) {
        atom.notifications.addWarning(
          'ansible-lint < 5.0 is unsupported. Backwards compatibility should exist, but is not guaranteed.',
          {
            detail: 'Please upgrade your version of ansible-lint to >= 5.0.',
            dismissable: true
          }
        );
      }
    });
  },

  deactivate() {
    this.idleCallbacks.forEach((callbackID) => window.cancelIdleCallback(callbackID));
    this.idleCallbacks.clear();
    this.subscriptions.dispose();
  },

  provideLinter() {
    return {
      name: 'Ansible-Lint',
      grammarScopes: ['source.ansible', 'source.ansible-advanced'],
      scope: 'project',
      lintsOnChange: false,
      lint: async (textEditor) => {
        // setup variables
        const helpers = require('atom-linter');
        const file = textEditor.getPath();
        const dir = require('path').dirname(file);

        // bail out if this is on the blacklist
        if (atom.config.get('linter-ansible-linting.blacklist') !== '') {
          const blacklist = new RegExp(atom.config.get('linter-ansible-linting.blacklist'))
          if (blacklist.exec(file))
            return [];
        }

        // cannot cwd in project path and then add file relative path to args because ansible relies on pathing relative to directory execution for includes
        let projectPath = atom.project.relativizePath(dir)[0];

        // pep8 output and no color
        let args = ['-p', '--nocolor', '--offline'];

        // add parseable severity flag if requested
        if (atom.config.get('linter-ansible-linting.displaySeverity'))
          args.push('--parseable-severity');

        // use config file if specified
        if (atom.config.get('linter-ansible-linting.useProjectConfig')) {
          // use ansible-lint config file in root project level
          args.push(...['-c', projectPath + '/.ansible-lint']);
        }
        else {
          // skip checks that user has opted to skip
          if (atom.config.get('linter-ansible-linting.ruleSkips') !== '')
            args.push(...['-x', atom.config.get('linter-ansible-linting.ruleSkips')]);

          // add additional rules directories
          if (atom.config.get('linter-ansible-linting.rulesDirs')[0] !== '') {
            if (atom.config.get('linter-ansible-linting.rulesDirDefault'))
              args.push('-R');

            for (i = 0; i < atom.config.get('linter-ansible-linting.rulesDirs').length; i++)
              args.push(...['-r', atom.config.get('linter-ansible-linting.rulesDirs')[i]]);
          }

          // exclude certain directories from checks
          if (atom.config.get('linter-ansible-linting.excludeDirs')[0] != '')
            for (i = 0; i < atom.config.get('linter-ansible-linting.excludeDirs').length; i++)
              args.push(...['--exclude', atom.config.get('linter-ansible-linting.excludeDirs')[i]]);
        }

        // lint file if playbook, else lint project
        if ((atom.config.get('linter-ansible-linting.lintProject')) && (!(/hosts:/.exec(textEditor.getText()))) && (!(projectPath == null)))
          args.push(projectPath);
        else
          args.push(file);

        // fix file not in a project; need that null coalescing operator
        projectPath = projectPath == null ? '' : projectPath + require('path').sep;

        return helpers.exec(atom.config.get('linter-ansible-linting.ansibleLintExecutablePath'), args, {cwd: projectPath, stream: 'both', timeout: atom.config.get('linter-ansible-linting.timeout') * 1000}).then(output => {
          // initialize variable for linter return here for either linter output or errors
          let toReturn = [];

          output.stdout.split(/\r?\n/).forEach((line) => {
            // setup matchers
            const lintMatches = /(.*.ya?ml):(\d+):\s([a-z-]+)\s(.*)/.exec(line);
            const lintMatchesSyntax = /(.*.ya?ml):(\d+):(\d+):\s([a-z-]+)\s(.*)/.exec(line);
            const lintMatchesGeneric = /(\w+:\d+:.*)/.exec(line);
            const lintMatchesSyntaxUnknown = /Ansible syntax check failed/.exec(line);

            // check for ansible-lint 5 syntax issues with unknown info
            if (lintMatchesSyntaxUnknown != null) {
              toReturn.push({
                severity: 'error',
                excerpt: 'Ansible syntax check failed for unknown reason; use syntax checker for more information.',
                location: {
                  file,
                  position: [[0, 0], [0, 1]],
                },
              });
            }
            // check for ansible-lint 5 output linting issues
            else if (lintMatches != null) {
              toReturn.push({
                severity: 'warning',
                excerpt: lintMatches[3] + ': ' + lintMatches[4],
                location: {
                  // ansible-lint plus atom-linter combined now lose pathing info for this kind of parsing
                  // prepend dir to file
                  file: projectPath + lintMatches[1],
                  position: [[Number.parseInt(lintMatches[2]) - 1, 0], [Number.parseInt(lintMatches[2]) - 1, 1]],
                },
              });
            }
            // check for ansible-lint 5 syntax issues with info
            else if (lintMatchesSyntax != null) {
              toReturn.push({
                severity: 'error',
                excerpt: lintMatchesSyntax[4] + ': ' + lintMatchesSyntax[5],
                location: {
                  // ansible-lint plus atom-linter combined now lose pathing info for this kind of parsing
                  // prepend dir to file
                  file: projectPath + lintMatchesSyntax[1],
                  position: [[Number.parseInt(lintMatchesSyntax[2]) - 1, Number.parseInt(lintMatchesSyntax[3]) - 1], [Number.parseInt(lintMatchesSyntax[2]) - 1, Number.parseInt(lintMatchesSyntax[3])]],
                },
              });
            }
            // check for generic failure in version five (super greedy, so must be last conditional)
            else if (lintMatchesGeneric != null) {
              toReturn.push({
                severity: 'warning',
                excerpt: lintMatchesGeneric[1],
                location: {
                  file: file,
                  position: [[0, 0], [0, 1]],
                },
              });
            }
          });
          // parse stderr
          const stderr = output.stderr;
          const stdinMatches = /\.dirname/.exec(stderr);
          const extraOutputMatches = /skip specific rules/.exec(stderr);
          const warningMatches = /WARNING/.exec(stderr);

          // check for stdin lint attempt
          if (stdinMatches != null) {
            toReturn.push({
              severity: 'info',
              excerpt: 'Ansible-Lint cannot reliably lint on stdin due to nonexistent pathing on includes and roles. Please save this playbook to your filesystem.',
              location: {
                file: 'Save this playbook.',
                position: [[0, 0], [0, 1]],
              },
            });
          }
          // output other errors directly to Atom notification display, and ignore noisy output
          else if ((extraOutputMatches == null) && (warningMatches == null) && (stderr !== '')) {
            atom.notifications.addError(
              'An unexpected error with ansible, ansible-lint, linter-ansible-linting, atom, linter, and/or your playbook/role has occurred.',
              { detail: stderr }
            );
          }
          return toReturn;
        });
      }
    };
  }
};
<|endoftext|>


<commit_before>'use babel'
import { CompositeDisposable } from 'atom'
import path from 'path'

let subs = null
let toolbar = null
let juliaClient = null
let HTMLPreviewView = require('./html-preview.js')

export function activate () {
    subs = new CompositeDisposable()

    subs.add(atom.workspace.addOpener((uri) => {
        if (path.extname(uri) === '.html' && uri.startsWith('preview://')) {
            return new HTMLPreviewView(uri)
        }
    }))

    subs.add(atom.commands.add(
        `atom-text-editor[data-grammar="source weave md"],
         atom-text-editor[data-grammar="source weave latex"]`,
        'weave:weave-to-pdf', () => weave('pdf')
    ))
    subs.add(atom.commands.add(
        `atom-text-editor[data-grammar="source weave md"],
         atom-text-editor[data-grammar="source weave latex"]`,
        'weave:weave-to-html', () => weave('html')
    ))
}

function weave(format) {
    if (juliaClient === null) {
        atom.notifications.addError("Juno not installed", {
            description: "[Juno](http://junolab.org/) needs to be installed to weave files."
        })
        return
    }

    juliaClient.boot()

    const ed = atom.workspace.getActiveTextEditor()
    const pane = atom.workspace.getActivePane()
    if (!ed) {
        atom.notifications.addError("No editor selected", {
            description: "Select an editor to weave a file."
        })
        return
    }
    let edpath = ed.getPath()
    if (!edpath) {
        atom.notifications.addError("File is not saved", {
            description: "Please save your file to weave it."
        })
        return
    }

    ed.save()

    if (process.platform === 'win32') {
        edpath = edpath.replace(/\\/g, "/")
    }
    let outpath = edpath.split('.')
    outpath.pop()
    outpath = outpath.join('.') + '.' + format

    let weaveCommand  = 'using Weave;'
    weaveCommand += 'try;'
    weaveCommand += `weave("${edpath}", doctype="md2${format}"); 1;`
    weaveCommand += 'catch err;'
    weaveCommand += '@error("Weaving failed.", error=err); 0;'
    weaveCommand += 'end'

    const evalsimple = juliaClient.import({rpc: ['evalsimple']}).evalsimple
    evalsimple(weaveCommand).then((res) => {
        if (!atom.config.get('language-weave.showPreview')) return

        if (res == 1) {
            if (format == 'html') {
                outpath = 'preview://' + outpath
            }
            if (
                format === 'pdf' && (
                    !atom.packages.isPackageActive('pdf-view') ||
                    !atom.packages.isPackageActive('pdf-view-plus')
                )
            ) {
                atom.notifications.addError("Can't display PDF", {
                    description: 'Please install the [pdf-view-plus](https://github.com/Aerijo/atom-pdf-view-plus) package.'
                })
                return
            }
            atom.workspace.open(outpath, {
                searchAllPanes: true,
                split: 'right'
            }).then(() => {
                pane.activate()
                pane.activateItem(ed)
            })
        } else {
            atom.notifications.addError("Weaving file failed", {
                description: "See the REPL for details."
            })
        }
    })
}

export function consumeJuliaClient (client) {
    juliaClient = client
}

export function consumeToolBar (getToolBar) {
    if (!atom.config.get('julia-client.uiOptions.enableToolBar')) return
    toolbar = getToolBar()
    toolbar.addSpacer()
    toolbar.addButton({
        icon: 'logo-html5',
        iconset: 'ion',
        callback: 'weave:weave-to-html',
        tooltip: 'Weave HTML'
    })
    toolbar.addButton({
        icon: 'file-pdf',
        iconset: 'fa',
        callback: 'weave:weave-to-pdf',
        tooltip: 'Weave PDF'
    })
    toolbar.addSpacer()
}

export var config = {
    showPreview: {
        type: 'boolean',
        default: true,
        title: 'Show Previews'
    }
}

export function deactivate () {
    if (toolbar) {
        toolbar.removeItems()
        toolbar = null
    }
    subs.dispose()
}

export function deserialize({filePath, uri}) {
    if (require('fs-plus').isFileSync(filePath)) {
        return new HTMLPreviewView(uri)
    }
}
<commit_msg>add weave command for source.julia scope<commit_after>'use babel'
import { CompositeDisposable } from 'atom'
import path from 'path'

let subs = null
let toolbar = null
let juliaClient = null
let HTMLPreviewView = require('./html-preview.js')

export function activate () {
    subs = new CompositeDisposable()

    subs.add(atom.workspace.addOpener((uri) => {
        if (path.extname(uri) === '.html' && uri.startsWith('preview://')) {
            return new HTMLPreviewView(uri)
        }
    }))

    subs.add(atom.commands.add(
        `atom-text-editor[data-grammar="source julia"],
         atom-text-editor[data-grammar="source weave md"],
         atom-text-editor[data-grammar="source weave latex"]`,
        'weave:weave-to-pdf', () => weave('pdf')
    ))
    subs.add(atom.commands.add(
        `atom-text-editor[data-grammar="source julia"],
         atom-text-editor[data-grammar="source weave md"],
         atom-text-editor[data-grammar="source weave latex"]`,
        'weave:weave-to-html', () => weave('html')
    ))
}

function weave(format) {
    if (juliaClient === null) {
        atom.notifications.addError("Juno not installed", {
            description: "[Juno](http://junolab.org/) needs to be installed to weave files."
        })
        return
    }

    juliaClient.boot()

    const ed = atom.workspace.getActiveTextEditor()
    const pane = atom.workspace.getActivePane()
    if (!ed) {
        atom.notifications.addError("No editor selected", {
            description: "Select an editor to weave a file."
        })
        return
    }
    let edpath = ed.getPath()
    if (!edpath) {
        atom.notifications.addError("File is not saved", {
            description: "Please save your file to weave it."
        })
        return
    }

    ed.save()

    if (process.platform === 'win32') {
        edpath = edpath.replace(/\\/g, "/")
    }
    let outpath = edpath.split('.')
    outpath.pop()
    outpath = outpath.join('.') + '.' + format

    let weaveCommand  = 'using Weave;'
    weaveCommand += 'try;'
    weaveCommand += `weave("${edpath}", doctype="md2${format}"); 1;`
    weaveCommand += 'catch err;'
    weaveCommand += '@error("Weaving failed.", error=err); 0;'
    weaveCommand += 'end'

    const evalsimple = juliaClient.import({rpc: ['evalsimple']}).evalsimple
    evalsimple(weaveCommand).then((res) => {
        if (!atom.config.get('language-weave.showPreview')) return

        if (res == 1) {
            if (format == 'html') {
                outpath = 'preview://' + outpath
            }
            if (
                format === 'pdf' && (
                    !atom.packages.isPackageActive('pdf-view') ||
                    !atom.packages.isPackageActive('pdf-view-plus')
                )
            ) {
                atom.notifications.addError("Can't display PDF", {
                    description: 'Please install the [pdf-view-plus](https://github.com/Aerijo/atom-pdf-view-plus) package.'
                })
                return
            }
            atom.workspace.open(outpath, {
                searchAllPanes: true,
                split: 'right'
            }).then(() => {
                pane.activate()
                pane.activateItem(ed)
            })
        } else {
            atom.notifications.addError("Weaving file failed", {
                description: "See the REPL for details."
            })
        }
    })
}

export function consumeJuliaClient (client) {
    juliaClient = client
}

export function consumeToolBar (getToolBar) {
    if (!atom.config.get('julia-client.uiOptions.enableToolBar')) return
    toolbar = getToolBar()
    toolbar.addSpacer()
    toolbar.addButton({
        icon: 'logo-html5',
        iconset: 'ion',
        callback: 'weave:weave-to-html',
        tooltip: 'Weave HTML'
    })
    toolbar.addButton({
        icon: 'file-pdf',
        iconset: 'fa',
        callback: 'weave:weave-to-pdf',
        tooltip: 'Weave PDF'
    })
    toolbar.addSpacer()
}

export var config = {
    showPreview: {
        type: 'boolean',
        default: true,
        title: 'Show Previews'
    }
}

export function deactivate () {
    if (toolbar) {
        toolbar.removeItems()
        toolbar = null
    }
    subs.dispose()
}

export function deserialize({filePath, uri}) {
    if (require('fs-plus').isFileSync(filePath)) {
        return new HTMLPreviewView(uri)
    }
}
<|endoftext|>


<commit_before>'use strict';

const {ipcMain} = require('electron');
const fs = require('fs');
const EventEmitter = require('events');
const protocols = require('electron-protocols');
const ipcPlus = require('electron-ipc-plus');
const path = require('path');
const fsJetpack = require('fs-jetpack');

const schema = require('./utils/schema');

let _url2profile = {};
let _url2schema = {};

let _type2path = {};
let _type2parent = {};

// register profile://
protocols.register('profile', uri => {
  let base = _type2path[uri.hostname];
  if ( !base ) {
    return null;
  }

  if ( uri.pathname ) {
    return path.join( base, uri.pathname );
  }

  return base;
});

// ==========================
// exports
// ==========================

/**
 * @module profile
 *
 * module for operating Profile class.
 */
let profile = {};
module.exports = profile;

/**
 * @method load
 * @param {string} url - The url of the profile.
 * @return {object} A Profile instance.
 * @see profile.register
 *
 * Load profile via `url`.
 * You must register your profile path via {@link profile.register} before you
 * can use it.
 *
 * @example
 *
 * ```js
 * const profile = require('electron-profile');
 *
 * // register a project profile
 * profile.register( 'project', '~/foo/bar');
 *
 * // load the profile at ~/foo/bar/foobar.json
 * let foobar = profile.load('profile://project/foobar.json', {
 *   foo: 'foo',
 *   bar: 'bar',
 * });
 *
 * // change and save your profile
 * foobar.data.foo = 'hello foo';
 * foobar.save();
 * ```
 */
profile.load = function ( url ) {
  if ( url.indexOf('profile://') !== 0 ) {
    console.error(`Failed to load profile ${url}: invalid protocol.`);
    return;
  }

  let path = protocols.path(url);
  if ( !path ) {
    console.error(`Failed to load profile ${url}: profile type not found.`);
    return null;
  }

  // There is already a cache
  if ( _url2profile[url] ) {
    return _url2profile[url];
  }

  let pobj;
  if ( !fs.existsSync(path) ) {
    pobj = {};
    _saveJson(path, pobj);
  } else {
    try {
      pobj = JSON.parse(fs.readFileSync(path));
    } catch ( err ) {
      if ( err ) {
        console.warn(`Failed to load profile ${url}: ${err.message}`);
        pobj = {};
      }
    }
  }

  let profileInst = new _Profile(url, pobj);
  // Add the cache
  _url2profile[url] = profileInst;

  return profileInst;
};

/**
 * @method register
 * @param {string} type - The type of the profile you want to register.
 * @param {string} path - The path for the register type.
 *
 * Register profile type with the path you provide.
 * {{#crossLink "profile.load"}}{{/crossLink}}
 */
profile.register = function ( type, path ) {
  _type2path[type] = path;
};

/**
 * @method inherit
 * @param {string} type - Current type
 * @param {string} parent - The parent type
 *
 * Set up a type of inheritance relationship
 * When the data cannot be found, it will query the data at the parent type
 */
profile.inherit = function ( type, parent ) {
  if (!_type2path[parent]) {
    console.warn(`The type of inheritance is not registered yet - ${parent}`);
  }
  _type2parent[type] = parent;
};

/**
 * @method reset
 *
 * Reset the registered profiles
 */
profile.reset = function () {
  _type2path = {};
};

/**
 * @method getPath
 *
 * Get path by type
 */
profile.getPath = function ( type ) {
  return _type2path[type];
};

/**
 * @method registerSchema
 *
 * Register schema for specific url
 */
profile.registerSchema = function ( url, json ) {
  if (!_url2profile[url] || !json) {
    return;
  }

  if ( url.indexOf('profile://') !== 0 ) {
    console.error(`Failed to load profile ${url}: invalid protocol.`);
    return;
  }

  let type = url.match(/^profile:\/\/([^\/]+)/)[1];

  if (_type2parent[type]) {
    console.log(`Failed to register schema ${url}: only root profile can register schema`);
  }
  _url2schema[url] = schema.json2schema(json);
};

// ==========================
// Internal
// ==========================

function _saveJson (file, object) {
  let dir = path.dirname(file);
  fsJetpack.dir(dir);
  fs.writeFileSync(file, JSON.stringify(object, null, 2), 'utf-8');
};

class _Profile extends EventEmitter {
  constructor ( url, data ) {
    super();
    this._url = url;
    this._data = data;

    let match = url.match(/^profile\:\/\/([^\/]+)\/(.*)$/);
    this._type = match[1];
    this._file = match[2];
  }

  get (key) {
    let schema = this.getSchema(this._type, this._file);
    let target = this._data[key];

    // Find the data on the inheritance chain
    let type = this._type;
    while (target === undefined && _type2parent[type]) {
      let parent = _type2parent[type];
      let parentUrl = `profile://${parent}/${this._file}`;
      let patentProfile = _url2profile[parentUrl];
      target = patentProfile ? patentProfile._data[key] : undefined;
      type = parent;
    }

    if (schema && target === undefined) {
      let property = schema.properties[key];
      target = property ? property.default : target;
    }

    // return a mutation object
    if (target && typeof target === 'object') {
      target = JSON.parse(JSON.stringify(target));
    }
    return target;
  }

  set (key, value) {
    let json = this.getSchema(this._type, this._file);
    let error = schema.validate(json, key, value);
    if (error) {
      return console.warn(`Failed to set profile ${key}: ${error}`);
    }

    this._data[key] = value;
  }

  remove (key) {
    delete this._data[key];
  }

  save () {
    let path = protocols.path(this._url);
    let json = JSON.stringify(this._data, null, 2);
    _saveJson(path, json);
    this.emit('changed');
    ipcPlus.sendToWins('electron-profile:changed', this._url, this._data);
  }

  reload () {
    let path = protocols.path(this._url);
    let jsonObj = JSON.parse(fs.readFileSync(path));

    for ( let p in jsonObj ) {
      this._data[p] = jsonObj[p];
    }

    this.emit('changed');
    ipcPlus.sendToWins('electron-profile:changed', this._url, this._data);
  }

  clear () {
    this._data = {};
  }

  reset ( data ) {
    this._data = data;
  }

  getSchema () {
    let type = this._type;
    while (_type2parent[type]) {
      type = _type2parent[type];
    }
    return _url2schema[`profile://${type}/${this._file}`];
  }

} // end class Profile

// ==========================
// Ipc
// ==========================

ipcMain.on('electron-profile:load', ( event, url ) => {
  let pobj = profile.load(url);
  let data = pobj ? pobj.data : null;
  event.reply( null, data );
});

ipcMain.on('electron-profile:save', ( event, url, data ) => {
  let pobj = profile.load(url);
  if ( pobj ) {
    pobj.reset(data);
    pobj.save();
  }
});
<commit_msg>Fixed a bug about save data<commit_after>'use strict';

const {ipcMain} = require('electron');
const fs = require('fs');
const EventEmitter = require('events');
const protocols = require('electron-protocols');
const ipcPlus = require('electron-ipc-plus');
const path = require('path');
const fsJetpack = require('fs-jetpack');

const schema = require('./utils/schema');

let _url2profile = {};
let _url2schema = {};

let _type2path = {};
let _type2parent = {};

// register profile://
protocols.register('profile', uri => {
  let base = _type2path[uri.hostname];
  if ( !base ) {
    return null;
  }

  if ( uri.pathname ) {
    return path.join( base, uri.pathname );
  }

  return base;
});

// ==========================
// exports
// ==========================

/**
 * @module profile
 *
 * module for operating Profile class.
 */
let profile = {};
module.exports = profile;

/**
 * @method load
 * @param {string} url - The url of the profile.
 * @return {object} A Profile instance.
 * @see profile.register
 *
 * Load profile via `url`.
 * You must register your profile path via {@link profile.register} before you
 * can use it.
 *
 * @example
 *
 * ```js
 * const profile = require('electron-profile');
 *
 * // register a project profile
 * profile.register( 'project', '~/foo/bar');
 *
 * // load the profile at ~/foo/bar/foobar.json
 * let foobar = profile.load('profile://project/foobar.json', {
 *   foo: 'foo',
 *   bar: 'bar',
 * });
 *
 * // change and save your profile
 * foobar.data.foo = 'hello foo';
 * foobar.save();
 * ```
 */
profile.load = function ( url ) {
  if ( url.indexOf('profile://') !== 0 ) {
    console.error(`Failed to load profile ${url}: invalid protocol.`);
    return;
  }

  let path = protocols.path(url);
  if ( !path ) {
    console.error(`Failed to load profile ${url}: profile type not found.`);
    return null;
  }

  // There is already a cache
  if ( _url2profile[url] ) {
    return _url2profile[url];
  }

  let pobj;
  if ( !fs.existsSync(path) ) {
    pobj = {};
    _saveJson(path, pobj);
  } else {
    try {
      pobj = JSON.parse(fs.readFileSync(path));
    } catch ( err ) {
      if ( err ) {
        console.warn(`Failed to load profile ${url}: ${err.message}`);
        pobj = {};
      }
    }
  }

  let profileInst = new _Profile(url, pobj);
  // Add the cache
  _url2profile[url] = profileInst;

  return profileInst;
};

/**
 * @method register
 * @param {string} type - The type of the profile you want to register.
 * @param {string} path - The path for the register type.
 *
 * Register profile type with the path you provide.
 * {{#crossLink "profile.load"}}{{/crossLink}}
 */
profile.register = function ( type, path ) {
  _type2path[type] = path;
};

/**
 * @method inherit
 * @param {string} type - Current type
 * @param {string} parent - The parent type
 *
 * Set up a type of inheritance relationship
 * When the data cannot be found, it will query the data at the parent type
 */
profile.inherit = function ( type, parent ) {
  if (!_type2path[parent]) {
    console.warn(`The type of inheritance is not registered yet - ${parent}`);
  }
  _type2parent[type] = parent;
};

/**
 * @method reset
 *
 * Reset the registered profiles
 */
profile.reset = function () {
  _type2path = {};
};

/**
 * @method getPath
 *
 * Get path by type
 */
profile.getPath = function ( type ) {
  return _type2path[type];
};

/**
 * @method registerSchema
 *
 * Register schema for specific url
 */
profile.registerSchema = function ( url, json ) {
  if (!_url2profile[url] || !json) {
    return;
  }

  if ( url.indexOf('profile://') !== 0 ) {
    console.error(`Failed to load profile ${url}: invalid protocol.`);
    return;
  }

  let type = url.match(/^profile:\/\/([^\/]+)/)[1];

  if (_type2parent[type]) {
    console.log(`Failed to register schema ${url}: only root profile can register schema`);
  }
  _url2schema[url] = schema.json2schema(json);
};

// ==========================
// Internal
// ==========================

function _saveJson (file, object) {
  let dir = path.dirname(file);
  fsJetpack.dir(dir);
  fs.writeFileSync(file, JSON.stringify(object, null, 2), 'utf-8');
};

class _Profile extends EventEmitter {
  constructor ( url, data ) {
    super();
    this._url = url;
    this._data = data;

    let match = url.match(/^profile\:\/\/([^\/]+)\/(.*)$/);
    this._type = match[1];
    this._file = match[2];
  }

  get (key) {
    let schema = this.getSchema(this._type, this._file);
    let target = this._data[key];

    // Find the data on the inheritance chain
    let type = this._type;
    while (target === undefined && _type2parent[type]) {
      let parent = _type2parent[type];
      let parentUrl = `profile://${parent}/${this._file}`;
      let patentProfile = _url2profile[parentUrl];
      target = patentProfile ? patentProfile._data[key] : undefined;
      type = parent;
    }

    if (schema && target === undefined) {
      let property = schema.properties[key];
      target = property ? property.default : target;
    }

    // return a mutation object
    if (target && typeof target === 'object') {
      target = JSON.parse(JSON.stringify(target));
    }
    return target;
  }

  set (key, value) {
    let json = this.getSchema(this._type, this._file);
    let error = schema.validate(json, key, value);
    if (error) {
      return console.warn(`Failed to set profile ${key}: ${error}`);
    }

    this._data[key] = value;
  }

  remove (key) {
    delete this._data[key];
  }

  save () {
    let path = protocols.path(this._url);
    _saveJson(path, this._data);
    this.emit('changed');
    ipcPlus.sendToWins('electron-profile:changed', this._url, this._data);
  }

  reload () {
    let path = protocols.path(this._url);
    let jsonObj = JSON.parse(fs.readFileSync(path));

    for ( let p in jsonObj ) {
      this._data[p] = jsonObj[p];
    }

    this.emit('changed');
    ipcPlus.sendToWins('electron-profile:changed', this._url, this._data);
  }

  clear () {
    this._data = {};
  }

  reset ( data ) {
    this._data = data;
  }

  getSchema () {
    let type = this._type;
    while (_type2parent[type]) {
      type = _type2parent[type];
    }
    return _url2schema[`profile://${type}/${this._file}`];
  }

} // end class Profile

// ==========================
// Ipc
// ==========================

ipcMain.on('electron-profile:load', ( event, url ) => {
  let pobj = profile.load(url);
  let data = pobj ? pobj.data : null;
  event.reply( null, data );
});

ipcMain.on('electron-profile:save', ( event, url, data ) => {
  let pobj = profile.load(url);
  if ( pobj ) {
    pobj.reset(data);
    pobj.save();
  }
});
<|endoftext|>