<commit_before>import { File } from "./file";
import { compareVersion } from "./util";
import { decodeScripts, encodeScripts, mergeScripts } from "./package-scripts";
import diff from "simple-diff";
const JSONPath = require("jsonpath");

function moduleNames(object) {
  if (object === undefined) return new Set();

  const modules = new Set();

  Object.keys(object).forEach(k => {
    const v = object[k];
    if (typeof v === "string") {
      modules.add(v);
    } else if (Array.isArray(v)) {
      v.forEach(e => {
        if (typeof e === "string") {
          modules.add(e);
        }
      });
    }
  });

  return modules;
}

/**
 * order in which json keys are written
 */
const sortedKeys = [
  "name",
  "version",
  "private",
  "publishConfig",
  "main",
  "browser",
  "module",
  "description",
  "keywords",
  "author",
  "maintainers",
  "contributors",
  "license",
  "bin",
  "scripts",
  "dependencies",
  "devDependencies",
  "peerDependencies",
  "optionalDependencies",
  "bundledDependencies",
  "engines",
  "os",
  "arch",
  "repository",
  "directories",
  "files",
  "man",
  "bugs",
  "homepage",
  "release",
  "ava",
  "nyc",
  "xo",
  "template"
];

/**
 * Merger for package.json
 */
export class Package extends File {
  static get defaultOptions() {
    return {
      actions: [],
      keywords: []
    };
  }

  static matchesFileName(name) {
    return name.match(/^package\.json$/);
  }

  optionalDevModules(modules = new Set()) {
    return new Set(["cracks", "dont-crack"].filter(m => modules.has(m)));
  }

  async usedDevModules(content) {
    content = await content;

    const pkg = content.length === 0 ? {} : JSON.parse(content);

    return moduleNames(pkg.release);
  }

  /**
   * Deliver some key properties
   * @param {Branch} branch
   * @return {Object}
   */
  async properties(branch) {
    try {
      const content = await branch.entry(this.name);
      const pkg = JSON.parse(content.content);

      const properties = {
        npm: { name: pkg.name, fullName: pkg.name }
      };

      if (pkg.name !== undefined) {
        const m = pkg.name.match(/^(\@[^\/]+)\/(.*)/);
        if (m) {
          properties.npm.organization = m[1];
          properties.npm.name = m[2];
        }
      }

      if (pkg.template !== undefined) {
        if (pkg.template.repository !== undefined) {
          properties.templateRepo = pkg.template.repository.url;
        }
        if (pkg.template.usedBy !== undefined) {
          properties.usedBy = pkg.template.usedBy;
        }
      }

      ["description", "version", "name", "main", "module", "browser"].forEach(
        key => {
          if (pkg[key] !== undefined && pkg[key] !== `{{${key}}}`) {
            if (!(key === "version" && pkg[key] === "0.0.0-semantic-release")) {
              properties[key] = pkg[key];
            }
          }
        }
      );

      return properties;
    } catch (e) {}

    return {};
  }

  async mergeContent(context, original, templateContent) {
    const originalLastChar = original[original.length - 1];
    const originalTemplate = JSON.parse(templateContent);

    const targetRepository = context.targetBranch.repository;

    let target =
      original === undefined || original === "" ? {} : JSON.parse(original);

    const template = Object.assign({}, originalTemplate, {
      repository: {
        type: targetRepository.type,
        url: targetRepository.url
      },
      bugs: {
        url: context.targetBranch.issuesURL
      },
      homepage: context.targetBranch.homePageURL,
      template: {
        repository: {
          url: context.templateBranch.url
        }
      }
    });
    template.template = Object.assign({}, target.template, template.template);

    let messages = [];
    const properties = context.properties;

    if (target.name === undefined || target.name === "") {
      const m = targetRepository.name.match(/^([^\/]+)\/(.*)/);
      target.name = m ? m[2] : context.targetBranch.name;
    }

    if (target.module !== undefined && !target.module.match(/\{\{module\}\}/)) {
      properties.module = target.module;
    }

    const slots = {
      repository: "chore(package): correct repository url",
      bugs: "chore(package): set bugs url from template",
      homepage: "chore(package): homepage from template",
      template: "chore(package): set template repo"
    };
    Object.keys(slots).forEach(key => {
      const templateValue = template[key];
      const d = diff(target[key], templateValue);

      if (
        templateValue !== undefined &&
        d.length > 0 &&
        !(
          d[0].type === "add" &&
          d[0].oldValue === undefined &&
          d[0].newValue === undefined
        )
      ) {
        messages.push(slots[key]);
        target[key] = templateValue;
      }
    });

    const decodedScripts = decodeScripts(target.scripts);

    const usedDevModules = await context.usedDevModules();

    context.debug({ usedDevModules: Array.from(usedDevModules) });

    const optionalDevModules = context.optionalDevModules(usedDevModules);

    context.debug({ optionalDevModules: Array.from(optionalDevModules) });

    const deepProperties = {
      devDependencies: { type: "chore", scope: "package", merge: defaultMerge },
      dependencies: { type: "fix", scope: "package", merge: defaultMerge },
      peerDependencies: { type: "fix", scope: "package", merge: defaultMerge },
      optionalDependencies: {
        type: "fix",
        scope: "package",
        merge: defaultMerge
      },
      bundeledDependencies: {
        type: "fix",
        scope: "package",
        merge: defaultMerge
      },
      scripts: { type: "chore", scope: "scripts", merge: defaultMerge },
      engines: { type: "chore", scope: "engines", merge: defaultMerge }
    };

    Object.keys(deepProperties).forEach(
      name => (deepProperties[name].name = name)
    );

    Object.keys(deepProperties).forEach(category => {
      if (template[category] !== undefined) {
        Object.keys(template[category]).forEach(d => {
          if (target[category] === undefined) {
            target[category] = {};
          }

          const tp = context.expand(template[category][d]);
          if (
            category === "devDependencies" &&
            target.dependencies !== undefined &&
            target.dependencies[d] === tp
          ) {
            // do not include dev dependency if regular dependency is already present
          } else {
            deepProperties[category].merge(
              target[category],
              target[category][d],
              tp,
              deepProperties[category],
              d,
              messages
            );
          }
        });
      }
    });

    Object.keys(template).forEach(p => {
      if (target[p] === undefined && target[p] !== "--delete--") {
        target[p] = template[p];
        messages.push(`chore(package): add ${p} from template`);
      }
    });

    target.scripts = encodeScripts(
      mergeScripts(decodedScripts, decodeScripts(template.scripts))
    );

    if (target.module === "{{module}}") {
      delete target.module;
    }

    if (
      target.contributors !== undefined &&
      target.author !== undefined &&
      target.author.name !== undefined
    ) {
      const m = target.author.name.match(/(^[^<]+)<([^>]+)>/);
      if (m !== undefined) {
        const name = String(m[1]).replace(/^\s+|\s+$/g, "");
        const email = m[2];

        if (
          target.contributors.find(c => c.name === name && c.email === email)
        ) {
          delete target.author;
        }
      }
    }

    const toBeDeletedModules =
      target.devDependencies === undefined
        ? []
        : Array.from(
            context.optionalDevModules(
              new Set(Object.keys(target.devDependencies))
            )
          ).filter(m => !usedDevModules.has(m));

    toBeDeletedModules.forEach(d => {
      messages = messages.filter(
        m => !m.startsWith(`chore(package): add ${d}@`)
      );
      delete target.devDependencies[d];
    });

    target = deleter(target, template, messages, []);

    Object.keys(this.options.keywords).forEach(r =>
      addKeyword(target, new RegExp(r), this.options.keywords[r], messages)
    );

    if (target.keywords !== undefined) {
      delete target.keywords["npm-package-template"];
    }

    removeKeyword(target, ["null", null, undefined], messages);

    this.options.actions.forEach(action => {
      if (action.op === "replace") {
        const v = JSONPath.value(template, action.path);
        if (v !== undefined) {
          const oldValue = JSONPath.value(target, action.path);
          if (oldValue !== v) {
            JSONPath.value(target, action.path, v);
            messages.push(
              `chore(package): set ${action.path}='${v}' as in template`
            );
          }
        }
      }
    });

    target = context.expand(target);
    const sortedTarget = normalizePackage(target);

    let newContent = JSON.stringify(sortedTarget, undefined, 2);
    const lastChar = newContent[newContent.length - 1];

    // keep trailing newline
    if (originalLastChar === "\n" && lastChar === "}") {
      newContent += "\n";
    }

    const changed = original !== newContent;

    if (changed && messages.length === 0) {
      messages.push("chore(package): update package.json from template");
    }

    return {
      content: newContent,
      messages,
      changed
    };
  }
}

function deleter(object, reference, messages, path) {
  if (
    typeof object === "string" ||
    object instanceof String ||
    object === true ||
    object === false ||
    object === undefined ||
    object === null ||
    typeof object === "number" ||
    object instanceof Number
  ) {
    return object;
  }

  if (Array.isArray(object)) {
    return object.map((e, i) => {
      path.push(i);
      const n = deleter(
        object[i],
        Array.isArray(reference) ? reference[i] : undefined,
        messages,
        path
      );
      path.pop();
      return n;
    });
  }

  if (reference) {
    Object.keys(reference).forEach(key => {
      path.push(key);

      if (reference[key] === "--delete--" && object[key] !== undefined) {
        if (object[key] !== "--delete--") {
          messages.push(`chore(package): delete ${path.join(".")}`);
        }
        delete object[key];
      } else {
        object[key] = deleter(object[key], reference[key], messages, path);
      }
      path.pop();
    });
  }

  return object;
}

function removeKeyword(pkg, keywords, messages) {
  if (pkg.keywords !== undefined) {
    keywords.forEach(keyword => {
      if (pkg.keywords.find(k => k === keyword)) {
        messages.push(`docs(package): remove keyword ${keyword}`);
        pkg.keywords = pkg.keywords.filter(k => k !== keyword);
      }
    });

    if (
      (pkg.keywords.length === 1 && pkg.keywords[0] === null) ||
      pkg.keywords[0] === undefined
    ) {
      messages.push(`docs(package): remove keyword null`);
      pkg.keywords = [];
    }
  }
}

function addKeyword(pkg, regex, keyword, messages) {
  if (keyword === undefined || keyword === null || keyword === "null") {
    return;
  }

  if (pkg.name.match(regex)) {
    if (pkg.keywords === undefined) {
      pkg.keywords = [];
    }
    if (!pkg.keywords.find(k => k === keyword)) {
      messages.push(`docs(package): add keyword ${keyword}`);
      pkg.keywords.push(keyword);
    }
  }
}

function getVersion(e) {
  const m = e.match(/([\d\.]+)/);
  return m ? Number(m[1]) : undefined;
}

function normalizeVersion(e) {
  return e.replace(/^[\^\$]/, "");
}

/**
 *
 */
function defaultMerge(destination, target, template, dp, name, messages) {
  if (template === "--delete--") {
    if (target !== undefined) {
      messages.push(`${dp.type}(${dp.scope}): remove ${name}@${target}`);
      delete destination[name];
    }

    return;
  }

  if (target === undefined) {
    messages.push(`${dp.type}(${dp.scope}): add ${name}@${template}`);
    destination[name] = template;
  } else if (template !== target) {
    if (dp.name === "engines") {
      if (getVersion(target) > getVersion(template)) {
        return;
      }
    }
    if (dp.name === "devDependencies") {
      //console.log(`${target} <> ${template} -> ${compareVersion(normalizeVersion(target), normalizeVersion(template))}`);
      if (
        compareVersion(normalizeVersion(target), normalizeVersion(template)) >=
        0
      ) {
        return;
      }
    }

    messages.push(`${dp.type}(${dp.scope}): ${name}@${template}`);

    destination[name] = template;
  }
}

/**
 * bring package into nomalized (sorted) form
 * @param {Object} source
 * @return {Object} normalized source
 */
function normalizePackage(source) {
  const normalized = {};

  sortedKeys.forEach(key => {
    if (source[key] !== undefined) {
      normalized[key] = source[key];
    }
  });

  Object.keys(source).forEach(key => {
    if (normalized[key] === undefined) {
      normalized[key] = source[key];
    }
  });

  return normalized;
}
<commit_msg>fix(package): list config as well known entry<commit_after>import { File } from "./file";
import { compareVersion } from "./util";
import { decodeScripts, encodeScripts, mergeScripts } from "./package-scripts";
import diff from "simple-diff";
const JSONPath = require("jsonpath");

function moduleNames(object) {
  if (object === undefined) return new Set();

  const modules = new Set();

  Object.keys(object).forEach(k => {
    const v = object[k];
    if (typeof v === "string") {
      modules.add(v);
    } else if (Array.isArray(v)) {
      v.forEach(e => {
        if (typeof e === "string") {
          modules.add(e);
        }
      });
    }
  });

  return modules;
}

/**
 * order in which json keys are written
 */
const sortedKeys = [
  "name",
  "version",
  "private",
  "publishConfig",
  "main",
  "browser",
  "module",
  "description",
  "keywords",
  "author",
  "maintainers",
  "contributors",
  "license",
  "bin",
  "scripts",
  "dependencies",
  "devDependencies",
  "peerDependencies",
  "optionalDependencies",
  "bundledDependencies",
  "engines",
  "os",
  "arch",
  "repository",
  "directories",
  "files",
  "man",
  "bugs",
  "homepage",
  "config",
  "release",
  "ava",
  "nyc",
  "xo",
  "template"
];

/**
 * Merger for package.json
 */
export class Package extends File {
  static get defaultOptions() {
    return {
      actions: [],
      keywords: []
    };
  }

  static matchesFileName(name) {
    return name.match(/^package\.json$/);
  }

  optionalDevModules(modules = new Set()) {
    return new Set(["cracks", "dont-crack"].filter(m => modules.has(m)));
  }

  async usedDevModules(content) {
    content = await content;

    const pkg = content.length === 0 ? {} : JSON.parse(content);

    return moduleNames(pkg.release);
  }

  /**
   * Deliver some key properties
   * @param {Branch} branch
   * @return {Object}
   */
  async properties(branch) {
    try {
      const content = await branch.entry(this.name);
      const pkg = JSON.parse(content.content);

      const properties = {
        npm: { name: pkg.name, fullName: pkg.name }
      };

      if (pkg.name !== undefined) {
        const m = pkg.name.match(/^(\@[^\/]+)\/(.*)/);
        if (m) {
          properties.npm.organization = m[1];
          properties.npm.name = m[2];
        }
      }

      if (pkg.template !== undefined) {
        if (pkg.template.repository !== undefined) {
          properties.templateRepo = pkg.template.repository.url;
        }
        if (pkg.template.usedBy !== undefined) {
          properties.usedBy = pkg.template.usedBy;
        }
      }

      ["description", "version", "name", "main", "module", "browser"].forEach(
        key => {
          if (pkg[key] !== undefined && pkg[key] !== `{{${key}}}`) {
            if (!(key === "version" && pkg[key] === "0.0.0-semantic-release")) {
              properties[key] = pkg[key];
            }
          }
        }
      );

      return properties;
    } catch (e) {}

    return {};
  }

  async mergeContent(context, original, templateContent) {
    const originalLastChar = original[original.length - 1];
    const originalTemplate = JSON.parse(templateContent);

    const targetRepository = context.targetBranch.repository;

    let target =
      original === undefined || original === "" ? {} : JSON.parse(original);

    const template = Object.assign({}, originalTemplate, {
      repository: {
        type: targetRepository.type,
        url: targetRepository.url
      },
      bugs: {
        url: context.targetBranch.issuesURL
      },
      homepage: context.targetBranch.homePageURL,
      template: {
        repository: {
          url: context.templateBranch.url
        }
      }
    });
    template.template = Object.assign({}, target.template, template.template);

    let messages = [];
    const properties = context.properties;

    if (target.name === undefined || target.name === "") {
      const m = targetRepository.name.match(/^([^\/]+)\/(.*)/);
      target.name = m ? m[2] : context.targetBranch.name;
    }

    if (target.module !== undefined && !target.module.match(/\{\{module\}\}/)) {
      properties.module = target.module;
    }

    const slots = {
      repository: "chore(package): correct repository url",
      bugs: "chore(package): set bugs url from template",
      homepage: "chore(package): homepage from template",
      template: "chore(package): set template repo"
    };
    Object.keys(slots).forEach(key => {
      const templateValue = template[key];
      const d = diff(target[key], templateValue);

      if (
        templateValue !== undefined &&
        d.length > 0 &&
        !(
          d[0].type === "add" &&
          d[0].oldValue === undefined &&
          d[0].newValue === undefined
        )
      ) {
        messages.push(slots[key]);
        target[key] = templateValue;
      }
    });

    const decodedScripts = decodeScripts(target.scripts);

    const usedDevModules = await context.usedDevModules();

    context.debug({ usedDevModules: Array.from(usedDevModules) });

    const optionalDevModules = context.optionalDevModules(usedDevModules);

    context.debug({ optionalDevModules: Array.from(optionalDevModules) });

    const deepProperties = {
      devDependencies: { type: "chore", scope: "package", merge: defaultMerge },
      dependencies: { type: "fix", scope: "package", merge: defaultMerge },
      peerDependencies: { type: "fix", scope: "package", merge: defaultMerge },
      optionalDependencies: {
        type: "fix",
        scope: "package",
        merge: defaultMerge
      },
      bundeledDependencies: {
        type: "fix",
        scope: "package",
        merge: defaultMerge
      },
      scripts: { type: "chore", scope: "scripts", merge: defaultMerge },
      engines: { type: "chore", scope: "engines", merge: defaultMerge }
    };

    Object.keys(deepProperties).forEach(
      name => (deepProperties[name].name = name)
    );

    Object.keys(deepProperties).forEach(category => {
      if (template[category] !== undefined) {
        Object.keys(template[category]).forEach(d => {
          if (target[category] === undefined) {
            target[category] = {};
          }

          const tp = context.expand(template[category][d]);
          if (
            category === "devDependencies" &&
            target.dependencies !== undefined &&
            target.dependencies[d] === tp
          ) {
            // do not include dev dependency if regular dependency is already present
          } else {
            deepProperties[category].merge(
              target[category],
              target[category][d],
              tp,
              deepProperties[category],
              d,
              messages
            );
          }
        });
      }
    });

    Object.keys(template).forEach(p => {
      if (target[p] === undefined && target[p] !== "--delete--") {
        target[p] = template[p];
        messages.push(`chore(package): add ${p} from template`);
      }
    });

    target.scripts = encodeScripts(
      mergeScripts(decodedScripts, decodeScripts(template.scripts))
    );

    if (target.module === "{{module}}") {
      delete target.module;
    }

    if (
      target.contributors !== undefined &&
      target.author !== undefined &&
      target.author.name !== undefined
    ) {
      const m = target.author.name.match(/(^[^<]+)<([^>]+)>/);
      if (m !== undefined) {
        const name = String(m[1]).replace(/^\s+|\s+$/g, "");
        const email = m[2];

        if (
          target.contributors.find(c => c.name === name && c.email === email)
        ) {
          delete target.author;
        }
      }
    }

    const toBeDeletedModules =
      target.devDependencies === undefined
        ? []
        : Array.from(
            context.optionalDevModules(
              new Set(Object.keys(target.devDependencies))
            )
          ).filter(m => !usedDevModules.has(m));

    toBeDeletedModules.forEach(d => {
      messages = messages.filter(
        m => !m.startsWith(`chore(package): add ${d}@`)
      );
      delete target.devDependencies[d];
    });

    target = deleter(target, template, messages, []);

    Object.keys(this.options.keywords).forEach(r =>
      addKeyword(target, new RegExp(r), this.options.keywords[r], messages)
    );

    if (target.keywords !== undefined) {
      delete target.keywords["npm-package-template"];
    }

    removeKeyword(target, ["null", null, undefined], messages);

    this.options.actions.forEach(action => {
      if (action.op === "replace") {
        const v = JSONPath.value(template, action.path);
        if (v !== undefined) {
          const oldValue = JSONPath.value(target, action.path);
          if (oldValue !== v) {
            JSONPath.value(target, action.path, v);
            messages.push(
              `chore(package): set ${action.path}='${v}' as in template`
            );
          }
        }
      }
    });

    target = context.expand(target);
    const sortedTarget = normalizePackage(target);

    let newContent = JSON.stringify(sortedTarget, undefined, 2);
    const lastChar = newContent[newContent.length - 1];

    // keep trailing newline
    if (originalLastChar === "\n" && lastChar === "}") {
      newContent += "\n";
    }

    const changed = original !== newContent;

    if (changed && messages.length === 0) {
      messages.push("chore(package): update package.json from template");
    }

    return {
      content: newContent,
      messages,
      changed
    };
  }
}

function deleter(object, reference, messages, path) {
  if (
    typeof object === "string" ||
    object instanceof String ||
    object === true ||
    object === false ||
    object === undefined ||
    object === null ||
    typeof object === "number" ||
    object instanceof Number
  ) {
    return object;
  }

  if (Array.isArray(object)) {
    return object.map((e, i) => {
      path.push(i);
      const n = deleter(
        object[i],
        Array.isArray(reference) ? reference[i] : undefined,
        messages,
        path
      );
      path.pop();
      return n;
    });
  }

  if (reference) {
    Object.keys(reference).forEach(key => {
      path.push(key);

      if (reference[key] === "--delete--" && object[key] !== undefined) {
        if (object[key] !== "--delete--") {
          messages.push(`chore(package): delete ${path.join(".")}`);
        }
        delete object[key];
      } else {
        object[key] = deleter(object[key], reference[key], messages, path);
      }
      path.pop();
    });
  }

  return object;
}

function removeKeyword(pkg, keywords, messages) {
  if (pkg.keywords !== undefined) {
    keywords.forEach(keyword => {
      if (pkg.keywords.find(k => k === keyword)) {
        messages.push(`docs(package): remove keyword ${keyword}`);
        pkg.keywords = pkg.keywords.filter(k => k !== keyword);
      }
    });

    if (
      (pkg.keywords.length === 1 && pkg.keywords[0] === null) ||
      pkg.keywords[0] === undefined
    ) {
      messages.push(`docs(package): remove keyword null`);
      pkg.keywords = [];
    }
  }
}

function addKeyword(pkg, regex, keyword, messages) {
  if (keyword === undefined || keyword === null || keyword === "null") {
    return;
  }

  if (pkg.name.match(regex)) {
    if (pkg.keywords === undefined) {
      pkg.keywords = [];
    }
    if (!pkg.keywords.find(k => k === keyword)) {
      messages.push(`docs(package): add keyword ${keyword}`);
      pkg.keywords.push(keyword);
    }
  }
}

function getVersion(e) {
  const m = e.match(/([\d\.]+)/);
  return m ? Number(m[1]) : undefined;
}

function normalizeVersion(e) {
  return e.replace(/^[\^\$]/, "");
}

/**
 *
 */
function defaultMerge(destination, target, template, dp, name, messages) {
  if (template === "--delete--") {
    if (target !== undefined) {
      messages.push(`${dp.type}(${dp.scope}): remove ${name}@${target}`);
      delete destination[name];
    }

    return;
  }

  if (target === undefined) {
    messages.push(`${dp.type}(${dp.scope}): add ${name}@${template}`);
    destination[name] = template;
  } else if (template !== target) {
    if (dp.name === "engines") {
      if (getVersion(target) > getVersion(template)) {
        return;
      }
    }
    if (dp.name === "devDependencies") {
      //console.log(`${target} <> ${template} -> ${compareVersion(normalizeVersion(target), normalizeVersion(template))}`);
      if (
        compareVersion(normalizeVersion(target), normalizeVersion(template)) >=
        0
      ) {
        return;
      }
    }

    messages.push(`${dp.type}(${dp.scope}): ${name}@${template}`);

    destination[name] = template;
  }
}

/**
 * bring package into nomalized (sorted) form
 * @param {Object} source
 * @return {Object} normalized source
 */
function normalizePackage(source) {
  const normalized = {};

  sortedKeys.forEach(key => {
    if (source[key] !== undefined) {
      normalized[key] = source[key];
    }
  });

  Object.keys(source).forEach(key => {
    if (normalized[key] === undefined) {
      normalized[key] = source[key];
    }
  });

  return normalized;
}
<|endoftext|>


<commit_before>import { File } from './file';
import { diffVersion } from './util';
import { value } from 'jsonpath';
import diff from 'simple-diff';

function moduleNames(object) {
  if (object === undefined) return new Set();

  const modules = new Set();

  Object.keys(object).forEach(k => {
    const v = object[k];
    if (typeof v === 'string') {
      modules.add(v);
    } else if (Array.isArray(v)) {
      v.forEach(e => {
        if (typeof e === 'string') {
          modules.add(e);
        }
      });
    }
  });

  return modules;
}

/**
 * order in which json keys are written
 */
const sortedKeys = [
  'name',
  'version',
  'private',
  'publishConfig',
  'main',
  'browser',
  'module',
  'description',
  'keywords',
  'author',
  'maintainers',
  'contributors',
  'license',
  'bin',
  'scripts',
  'dependencies',
  'devDependencies',
  'peerDependencies',
  'optionalDependencies',
  'bundledDependencies',
  'engines',
  'os',
  'arch',
  'repository',
  'directories',
  'files',
  'man',
  'bugs',
  'homepage',
  'release',
  'ava',
  'nyc',
  'xo',
  'template'
];

/**
 * Merger for package.json
 */
export class Package extends File {
  static decodeScripts(scripts) {
    const decoded = {};

    if (scripts !== undefined) {
      Object.keys(scripts).forEach(key => {
        const script = scripts[key];
        decoded[key] = script.split(/\s*&&\s*/);
      });
    }

    return decoded;
  }

  static get defaultOptions() {
    return {
      actions: [],
      keywords: []
    };
  }

  static matchesFileName(name) {
    return name.match(/^package\.json$/);
  }

  optionalDevModules(modules = new Set()) {
    return new Set(['cracks', 'dont-crack'].filter(m => modules.has(m)));
  }

  async usedDevModules(content) {
    content = await content;

    const pkg = content.length === 0 ? {} : JSON.parse(content);

    return moduleNames(pkg.release);
  }

  /**
   * Deliver some key properties
   * @param {Branch} branch
   * @return {Object}
   */
  async properties(branch) {
    try {
      const content = await branch.content(this.path);
      const pkg = JSON.parse(content.content);

      const properties = {
        npm: { name: pkg.name, fullName: pkg.name }
      };

      if (pkg.name !== undefined) {
        const m = pkg.name.match(/^(\@[^\/]+)\/(.*)/);
        if (m) {
          properties.npm.organization = m[1];
          properties.npm.name = m[2];
        }
      }

      if (pkg.template !== undefined && pkg.template.repository !== undefined) {
        properties.templateRepo = pkg.template.repository.url;
      }

      ['description', 'name', 'module', 'browser', 'version'].forEach(key => {
        if (pkg[key] !== undefined && pkg[key] !== `{{${key}}}`) {
          if (!(key === 'version' && pkg[key] === '0.0.0-semantic-release')) {
            properties[key] = pkg[key];
          }
        }
      });

      return properties;
    } catch (e) {}

    return {};
  }

  async mergeContent(context, original, templateContent) {
    const originalLastChar = original[original.length - 1];
    const originalTemplate = JSON.parse(templateContent);

    const targetRepository = context.targetBranch.repository;

    let target =
      original === undefined || original === '' ? {} : JSON.parse(original);

    const template = Object.assign({}, originalTemplate, {
      repository: {
        type: targetRepository.type,
        url: targetRepository.url
      },
      bugs: {
        url: context.targetBranch.issuesURL
      },
      homepage: context.targetBranch.homePageURL,
      template: {
        repository: {
          url: context.templateBranch.url
        }
      }
    });
    template.template = Object.assign({}, target.template, template.template);

    let messages = [];
    const properties = context.properties;

    if (target.name === undefined || target.name === '') {
      const m = targetRepository.name.match(/^([^\/]+)\/(.*)/);
      target.name = m ? m[2] : context.targetBranch.name;
    }

    if (target.module !== undefined && !target.module.match(/\{\{module\}\}/)) {
      properties.module = target.module;
    }

    properties.main =
      target.main && !target.main.match(/\{\{main\}\}/)
        ? target.main
        : 'dist/index.js';

    const slots = {
      repository: 'chore(package): correct repository url',
      bugs: 'chore(package): correct bugs url',
      homepage: 'chore(package): homepage',
      template: 'chore(package): set template repo'
    };
    Object.keys(slots).forEach(key => {
      if (
        template[key] !== undefined &&
        diff(target[key], template[key]).length > 0
      ) {
        messages.push(slots[key]);
        target[key] = template[key];
      }
    });

    const extraBuilds = {};

    if (target.scripts !== undefined) {
      Object.keys(target.scripts).forEach(key => {
        const script = target.scripts[key];

        if (template.scripts !== undefined) {
          const templateScript = template.scripts[key];
          if (templateScript !== undefined) {
            //const parts = script.split(/\s*&&\s*/);
            const mb = script.match(/&&\s*(.+)$/);
            if (mb && !templateScript.includes(mb[1])) {
              extraBuilds[key] = mb[1];
            }
          }
        }
      });
    }

    const usedDevModules = await context.usedDevModules();

    //context.debug(`usedDevModules: ${Array.from(usedDevModules).join(',')}`);

    const optionalDevModules = context.optionalDevModules(usedDevModules);

    /*context.debug(
      `optionalDevModules: ${Array.from(optionalDevModules).join(',')}`
    );*/

    const deepProperties = {
      devDependencies: { type: 'chore', scope: 'package', merge: defaultMerge },
      dependencies: { type: 'fix', scope: 'package', merge: defaultMerge },
      peerDependencies: { type: 'fix', scope: 'package', merge: defaultMerge },
      optionalDependencies: {
        type: 'fix',
        scope: 'package',
        merge: defaultMerge
      },
      bundeledDependencies: {
        type: 'fix',
        scope: 'package',
        merge: defaultMerge
      },
      scripts: { type: 'chore', scope: 'scripts', merge: defaultMerge },
      engines: { type: 'chore', scope: 'engines', merge: defaultMerge }
    };

    Object.keys(deepProperties).forEach(
      name => (deepProperties[name].name = name)
    );

    Object.keys(deepProperties).forEach(category => {
      if (template[category] !== undefined) {
        Object.keys(template[category]).forEach(d => {
          if (target[category] === undefined) {
            target[category] = {};
          }

          const tp = context.expand(template[category][d]);
          if (
            category === 'devDependencies' &&
            target.dependencies !== undefined &&
            target.dependencies[d] === tp
          ) {
            // do not include dev dependency if regular dependency is already present
          } else {
            deepProperties[category].merge(
              target[category],
              target[category][d],
              tp,
              deepProperties[category],
              d,
              messages
            );
          }
        });
      }
    });

    Object.keys(template).forEach(p => {
      if (target[p] === undefined) {
        target[p] = template[p];
        messages.push(`chore(package): add ${p} from template`);
      }
    });

    Object.keys(extraBuilds).forEach(key => {
      target.scripts[key] += ` && ${extraBuilds[key]}`;
    });

    if (target.module === '{{module}}') {
      delete target.module;
    }

    if (
      target.contributors !== undefined &&
      target.author !== undefined &&
      target.author.name !== undefined
    ) {
      const m = target.author.name.match(/(^[^<]+)<([^>]+)>/);
      if (m !== undefined) {
        const name = String(m[1]).replace(/^\s+|\s+$/g, '');
        const email = m[2];

        if (
          target.contributors.find(c => c.name === name && c.email === email)
        ) {
          delete target.author;
        }
      }
    }

    const toBeDeletedModules =
      target.devDependencies === undefined
        ? []
        : Array.from(
            context.optionalDevModules(
              new Set(Object.keys(target.devDependencies))
            )
          ).filter(m => !usedDevModules.has(m));

    toBeDeletedModules.forEach(d => {
      messages = messages.filter(
        m => !m.startsWith(`chore(package): add ${d}@`)
      );
      delete target.devDependencies[d];
    });

    target = deleter(target, template, messages, []);

    if (target.keywords !== undefined) {
      delete target.keywords['npm-package-template'];
    }

    Object.keys(this.options.keywords).forEach(r =>
      addKeyword(target, new RegExp(r), this.options.keywords[r], messages)
    );

    removeKeyword(target, ['null', null, undefined], messages);

    this.options.actions.forEach(action => {
      if (action.op === 'replace') {
        const v = value(template, action.path);
        if (v !== undefined) {
          const oldValue = value(target, action.path);
          if (oldValue !== v) {
            value(target, action.path, v);
            messages.push(
              `chore(package): set ${action.path}='${v}' as in template`
            );
          }
        }
      }
    });

    if (messages.length === 0) {
      messages.push('chore(package): update package.json from template');
    }

    target = context.expand(target);
    const sortedTarget = {};

    sortedKeys.forEach(key => {
      if (target[key] !== undefined) {
        sortedTarget[key] = target[key];
      }
    });

    Object.keys(target).forEach(key => {
      if (sortedTarget[key] === undefined) {
        sortedTarget[key] = target[key];
      }
    });

    let newContent = JSON.stringify(sortedTarget, undefined, 2);
    const lastChar = newContent[newContent.length - 1];

    // keep trailing newline
    if (originalLastChar === '\n' && lastChar === '}') {
      newContent += '\n';
    }

    return {
      content: newContent,
      messages,
      changed: original !== newContent
    };
  }
}

function deleter(object, reference, messages, path) {
  if (
    typeof object === 'string' ||
    object instanceof String ||
    object === true ||
    object === false ||
    object === undefined ||
    object === null ||
    typeof object === 'number' ||
    object instanceof Number
  ) {
    return object;
  }

  if (Array.isArray(object)) {
    return object.map((e, i) => {
      path.push(i);
      const n = deleter(
        object[i],
        Array.isArray(reference) ? reference[i] : undefined,
        messages,
        path
      );
      path.pop();
      return n;
    });
  }

  if (reference) {
    Object.keys(reference).forEach(key => {
      path.push(key);

      if (reference[key] === '--delete--' && object[key] !== undefined) {
        if (object[key] !== '--delete--') {
          messages.push(`chore(npm): delete ${path.join('.')}`);
        }
        delete object[key];
      } else {
        object[key] = deleter(object[key], reference[key], messages, path);
      }
      path.pop();
    });
  }

  return object;
}

function removeKeyword(pkg, keywords, messages) {
  if (pkg.keywords !== undefined) {
    keywords.forEach(keyword => {
      if (pkg.keywords.find(k => k === keyword)) {
        messages.push(`docs(package): remove keyword ${keyword}`);
        pkg.keywords = pkg.keywords.filter(k => k !== keyword);
      }
    });

    if (pkg.keywords[0] === null || pkg.keywords[0] === undefined) {
      messages.push(`docs(package): remove keyword null`);
      pkg.keywords = [];
    }
  }
}

function addKeyword(pkg, regex, keyword, messages) {
  if (keyword === undefined || keyword === null || keyword === 'null') {
    return;
  }

  if (pkg.name.match(regex)) {
    if (pkg.keywords === undefined) {
      pkg.keywords = [];
    }
    if (!pkg.keywords.find(k => k === keyword)) {
      messages.push(`docs(package): add keyword ${keyword}`);
      pkg.keywords.push(keyword);
    }
  }
}

function getVersion(e) {
  const m = e.match(/([\d\.]+)/);
  return m ? Number(m[1]) : undefined;
}

function normalizeVersion(e) {
  return e.replace(/^[\^\$]/, '');
}

/**
 *
 */
function defaultMerge(destination, target, template, dp, name, messages) {
  if (template === '-') {
    if (target !== undefined) {
      messages.push(`${dp.type}(${dp.scope}): remove ${name}@${target}`);
      delete destination[name];
    }

    return;
  }

  if (target === undefined) {
    messages.push(`${dp.type}(${dp.scope}): add ${name}@${template}`);
    destination[name] = template;
  } else if (template !== target) {
    if (dp.name === 'engines') {
      if (getVersion(target) > getVersion(template)) {
        return;
      }
    }
    if (dp.name === 'devDependencies') {
      if (
        diffVersion(normalizeVersion(target), normalizeVersion(template)) >= 0
      ) {
        return;
      }
    }

    messages.push(`${dp.type}(${dp.scope}): ${name}@${template}`);

    destination[name] = template;
  }
}
<commit_msg>refactor(package): insert fallback message only if content has changed<commit_after>import { File } from './file';
import { diffVersion } from './util';
import { value } from 'jsonpath';
import diff from 'simple-diff';

function moduleNames(object) {
  if (object === undefined) return new Set();

  const modules = new Set();

  Object.keys(object).forEach(k => {
    const v = object[k];
    if (typeof v === 'string') {
      modules.add(v);
    } else if (Array.isArray(v)) {
      v.forEach(e => {
        if (typeof e === 'string') {
          modules.add(e);
        }
      });
    }
  });

  return modules;
}

/**
 * order in which json keys are written
 */
const sortedKeys = [
  'name',
  'version',
  'private',
  'publishConfig',
  'main',
  'browser',
  'module',
  'description',
  'keywords',
  'author',
  'maintainers',
  'contributors',
  'license',
  'bin',
  'scripts',
  'dependencies',
  'devDependencies',
  'peerDependencies',
  'optionalDependencies',
  'bundledDependencies',
  'engines',
  'os',
  'arch',
  'repository',
  'directories',
  'files',
  'man',
  'bugs',
  'homepage',
  'release',
  'ava',
  'nyc',
  'xo',
  'template'
];

/**
 * Merger for package.json
 */
export class Package extends File {
  static decodeScripts(scripts) {
    const decoded = {};

    if (scripts !== undefined) {
      Object.keys(scripts).forEach(key => {
        const script = scripts[key];
        decoded[key] = script.split(/\s*&&\s*/);
      });
    }

    return decoded;
  }

  static get defaultOptions() {
    return {
      actions: [],
      keywords: []
    };
  }

  static matchesFileName(name) {
    return name.match(/^package\.json$/);
  }

  optionalDevModules(modules = new Set()) {
    return new Set(['cracks', 'dont-crack'].filter(m => modules.has(m)));
  }

  async usedDevModules(content) {
    content = await content;

    const pkg = content.length === 0 ? {} : JSON.parse(content);

    return moduleNames(pkg.release);
  }

  /**
   * Deliver some key properties
   * @param {Branch} branch
   * @return {Object}
   */
  async properties(branch) {
    try {
      const content = await branch.content(this.path);
      const pkg = JSON.parse(content.content);

      const properties = {
        npm: { name: pkg.name, fullName: pkg.name }
      };

      if (pkg.name !== undefined) {
        const m = pkg.name.match(/^(\@[^\/]+)\/(.*)/);
        if (m) {
          properties.npm.organization = m[1];
          properties.npm.name = m[2];
        }
      }

      if (pkg.template !== undefined && pkg.template.repository !== undefined) {
        properties.templateRepo = pkg.template.repository.url;
      }

      ['description', 'name', 'module', 'browser', 'version'].forEach(key => {
        if (pkg[key] !== undefined && pkg[key] !== `{{${key}}}`) {
          if (!(key === 'version' && pkg[key] === '0.0.0-semantic-release')) {
            properties[key] = pkg[key];
          }
        }
      });

      return properties;
    } catch (e) {}

    return {};
  }

  async mergeContent(context, original, templateContent) {
    const originalLastChar = original[original.length - 1];
    const originalTemplate = JSON.parse(templateContent);

    const targetRepository = context.targetBranch.repository;

    let target =
      original === undefined || original === '' ? {} : JSON.parse(original);

    const template = Object.assign({}, originalTemplate, {
      repository: {
        type: targetRepository.type,
        url: targetRepository.url
      },
      bugs: {
        url: context.targetBranch.issuesURL
      },
      homepage: context.targetBranch.homePageURL,
      template: {
        repository: {
          url: context.templateBranch.url
        }
      }
    });
    template.template = Object.assign({}, target.template, template.template);

    let messages = [];
    const properties = context.properties;

    if (target.name === undefined || target.name === '') {
      const m = targetRepository.name.match(/^([^\/]+)\/(.*)/);
      target.name = m ? m[2] : context.targetBranch.name;
    }

    if (target.module !== undefined && !target.module.match(/\{\{module\}\}/)) {
      properties.module = target.module;
    }

    properties.main =
      target.main && !target.main.match(/\{\{main\}\}/)
        ? target.main
        : 'dist/index.js';

    const slots = {
      repository: 'chore(package): correct repository url',
      bugs: 'chore(package): correct bugs url',
      homepage: 'chore(package): homepage',
      template: 'chore(package): set template repo'
    };
    Object.keys(slots).forEach(key => {
      if (
        template[key] !== undefined &&
        diff(target[key], template[key]).length > 0
      ) {
        messages.push(slots[key]);
        target[key] = template[key];
      }
    });

    const extraBuilds = {};

    if (target.scripts !== undefined) {
      Object.keys(target.scripts).forEach(key => {
        const script = target.scripts[key];

        if (template.scripts !== undefined) {
          const templateScript = template.scripts[key];
          if (templateScript !== undefined) {
            //const parts = script.split(/\s*&&\s*/);
            const mb = script.match(/&&\s*(.+)$/);
            if (mb && !templateScript.includes(mb[1])) {
              extraBuilds[key] = mb[1];
            }
          }
        }
      });
    }

    const usedDevModules = await context.usedDevModules();

    //context.debug(`usedDevModules: ${Array.from(usedDevModules).join(',')}`);

    const optionalDevModules = context.optionalDevModules(usedDevModules);

    /*context.debug(
      `optionalDevModules: ${Array.from(optionalDevModules).join(',')}`
    );*/

    const deepProperties = {
      devDependencies: { type: 'chore', scope: 'package', merge: defaultMerge },
      dependencies: { type: 'fix', scope: 'package', merge: defaultMerge },
      peerDependencies: { type: 'fix', scope: 'package', merge: defaultMerge },
      optionalDependencies: {
        type: 'fix',
        scope: 'package',
        merge: defaultMerge
      },
      bundeledDependencies: {
        type: 'fix',
        scope: 'package',
        merge: defaultMerge
      },
      scripts: { type: 'chore', scope: 'scripts', merge: defaultMerge },
      engines: { type: 'chore', scope: 'engines', merge: defaultMerge }
    };

    Object.keys(deepProperties).forEach(
      name => (deepProperties[name].name = name)
    );

    Object.keys(deepProperties).forEach(category => {
      if (template[category] !== undefined) {
        Object.keys(template[category]).forEach(d => {
          if (target[category] === undefined) {
            target[category] = {};
          }

          const tp = context.expand(template[category][d]);
          if (
            category === 'devDependencies' &&
            target.dependencies !== undefined &&
            target.dependencies[d] === tp
          ) {
            // do not include dev dependency if regular dependency is already present
          } else {
            deepProperties[category].merge(
              target[category],
              target[category][d],
              tp,
              deepProperties[category],
              d,
              messages
            );
          }
        });
      }
    });

    Object.keys(template).forEach(p => {
      if (target[p] === undefined) {
        target[p] = template[p];
        messages.push(`chore(package): add ${p} from template`);
      }
    });

    Object.keys(extraBuilds).forEach(key => {
      target.scripts[key] += ` && ${extraBuilds[key]}`;
    });

    if (target.module === '{{module}}') {
      delete target.module;
    }

    if (
      target.contributors !== undefined &&
      target.author !== undefined &&
      target.author.name !== undefined
    ) {
      const m = target.author.name.match(/(^[^<]+)<([^>]+)>/);
      if (m !== undefined) {
        const name = String(m[1]).replace(/^\s+|\s+$/g, '');
        const email = m[2];

        if (
          target.contributors.find(c => c.name === name && c.email === email)
        ) {
          delete target.author;
        }
      }
    }

    const toBeDeletedModules =
      target.devDependencies === undefined
        ? []
        : Array.from(
            context.optionalDevModules(
              new Set(Object.keys(target.devDependencies))
            )
          ).filter(m => !usedDevModules.has(m));

    toBeDeletedModules.forEach(d => {
      messages = messages.filter(
        m => !m.startsWith(`chore(package): add ${d}@`)
      );
      delete target.devDependencies[d];
    });

    target = deleter(target, template, messages, []);

    if (target.keywords !== undefined) {
      delete target.keywords['npm-package-template'];
    }

    Object.keys(this.options.keywords).forEach(r =>
      addKeyword(target, new RegExp(r), this.options.keywords[r], messages)
    );

    removeKeyword(target, ['null', null, undefined], messages);

    this.options.actions.forEach(action => {
      if (action.op === 'replace') {
        const v = value(template, action.path);
        if (v !== undefined) {
          const oldValue = value(target, action.path);
          if (oldValue !== v) {
            value(target, action.path, v);
            messages.push(
              `chore(package): set ${action.path}='${v}' as in template`
            );
          }
        }
      }
    });

    target = context.expand(target);
    const sortedTarget = {};

    sortedKeys.forEach(key => {
      if (target[key] !== undefined) {
        sortedTarget[key] = target[key];
      }
    });

    Object.keys(target).forEach(key => {
      if (sortedTarget[key] === undefined) {
        sortedTarget[key] = target[key];
      }
    });

    let newContent = JSON.stringify(sortedTarget, undefined, 2);
    const lastChar = newContent[newContent.length - 1];

    // keep trailing newline
    if (originalLastChar === '\n' && lastChar === '}') {
      newContent += '\n';
    }

    const changed = original !== newContent;

    if (changed && messages.length === 0) {
      messages.push('chore(package): update package.json from template');
    }

    return {
      content: newContent,
      messages,
      changed
    };
  }
}

function deleter(object, reference, messages, path) {
  if (
    typeof object === 'string' ||
    object instanceof String ||
    object === true ||
    object === false ||
    object === undefined ||
    object === null ||
    typeof object === 'number' ||
    object instanceof Number
  ) {
    return object;
  }

  if (Array.isArray(object)) {
    return object.map((e, i) => {
      path.push(i);
      const n = deleter(
        object[i],
        Array.isArray(reference) ? reference[i] : undefined,
        messages,
        path
      );
      path.pop();
      return n;
    });
  }

  if (reference) {
    Object.keys(reference).forEach(key => {
      path.push(key);

      if (reference[key] === '--delete--' && object[key] !== undefined) {
        if (object[key] !== '--delete--') {
          messages.push(`chore(npm): delete ${path.join('.')}`);
        }
        delete object[key];
      } else {
        object[key] = deleter(object[key], reference[key], messages, path);
      }
      path.pop();
    });
  }

  return object;
}

function removeKeyword(pkg, keywords, messages) {
  if (pkg.keywords !== undefined) {
    keywords.forEach(keyword => {
      if (pkg.keywords.find(k => k === keyword)) {
        messages.push(`docs(package): remove keyword ${keyword}`);
        pkg.keywords = pkg.keywords.filter(k => k !== keyword);
      }
    });

    if (pkg.keywords[0] === null || pkg.keywords[0] === undefined) {
      messages.push(`docs(package): remove keyword null`);
      pkg.keywords = [];
    }
  }
}

function addKeyword(pkg, regex, keyword, messages) {
  if (keyword === undefined || keyword === null || keyword === 'null') {
    return;
  }

  if (pkg.name.match(regex)) {
    if (pkg.keywords === undefined) {
      pkg.keywords = [];
    }
    if (!pkg.keywords.find(k => k === keyword)) {
      messages.push(`docs(package): add keyword ${keyword}`);
      pkg.keywords.push(keyword);
    }
  }
}

function getVersion(e) {
  const m = e.match(/([\d\.]+)/);
  return m ? Number(m[1]) : undefined;
}

function normalizeVersion(e) {
  return e.replace(/^[\^\$]/, '');
}

/**
 *
 */
function defaultMerge(destination, target, template, dp, name, messages) {
  if (template === '-') {
    if (target !== undefined) {
      messages.push(`${dp.type}(${dp.scope}): remove ${name}@${target}`);
      delete destination[name];
    }

    return;
  }

  if (target === undefined) {
    messages.push(`${dp.type}(${dp.scope}): add ${name}@${template}`);
    destination[name] = template;
  } else if (template !== target) {
    if (dp.name === 'engines') {
      if (getVersion(target) > getVersion(template)) {
        return;
      }
    }
    if (dp.name === 'devDependencies') {
      if (
        diffVersion(normalizeVersion(target), normalizeVersion(template)) >= 0
      ) {
        return;
      }
    }

    messages.push(`${dp.type}(${dp.scope}): ${name}@${template}`);

    destination[name] = template;
  }
}
<|endoftext|>


<commit_before>import File from './file';

function moduleNames(object) {
  if (object === undefined) return new Set();

  const modules = new Set();

  Object.keys(object).forEach(k => {
    const v = object[k];
    if (typeof v === 'string') {
      modules.add(v);
    } else if (Array.isArray(v)) {
      v.forEach(e => {
        if (typeof e === 'string') {
          modules.add(e);
        }
      });
    }
  });

  return modules;
}

export default class Package extends File {
  static matchesFileName(name) {
    return name.match(/^package\.json$/);
  }

  optionalDevModules(modules = new Set()) {
    return new Set(['cracks', 'dont-crack'].filter(m => modules.has(m)));
  }

  async usedDevModules(content) {
    content = await content;

    const pkg = content.length === 0 ? {} : JSON.parse(content);

    return moduleNames(pkg.release);
  }

  async templateRepo(context) {
    const content = await this.originalContent(context, {
      ignoreMissing: true
    });
    const pkg = JSON.parse(content);
    if (pkg.template !== undefined && pkg.template.repository !== undefined) {
      const m = pkg.template.repository.url.match(/github.com\/(.*)\.git$/);
      return m[1];
    }

    return undefined;
  }

  async mergeContent(context, original, templateContent) {
    const originalLastChar = original[original.length - 1];

    let target =
      original === undefined || original === '' ? {} : JSON.parse(original);
    const template = JSON.parse(templateContent);
    let messages = [];
    const properties = context.properties;

    if (target.name === undefined || target.name === '') {
      const m = context.targetRepo.name.match(/^([^\/]+)\/(.*)/);
      target.name = m ? m[2] : context.targetRepo.name;
    }

    if (target.module !== undefined && !target.module.match(/\{\{module\}\}/)) {
      properties.module = target.module;
    }

    properties.main =
      target.main && !target.main.match(/\{\{main\}\}/)
        ? target.main
        : 'dist/index.js';

    const githubURL = `git+https://github.com/${properties.user}/${target.name}.git`;
    const githubURLAlternative = `git+https://github.com/${properties.user}/node-${target.name}.git`;

    let repoName = target.name;

    if (
      target.repository === undefined ||
      (target.repository.url !== githubURL &&
        target.repository.url !== githubURLAlternative)
    ) {
      target.repository = {
        type: 'git',
        url: githubURL
      };
      messages.push(`chore(package): correct github url`);
    }

    if (
      target.repository !== undefined &&
      githubURLAlternative === target.repository.url
    ) {
      repoName = `node-${target.name}`;
    }

    const bugsURL = `https://github.com/${properties.user}/${repoName}/issues`;

    if (target.bugs === undefined || target.bugs.url !== bugsURL) {
      target.bugs = {
        url: bugsURL
      };
      messages.push(`chore(package): correct bugs url`);
    }

    const homepageURL = `https://github.com/${properties.user}/${repoName}#readme`;

    if (target.homepage === undefined || target.homepage !== homepageURL) {
      target.homepage = homepageURL;
      messages.push(`chore(package): correct hompage url`);
    }

    let buildOutput;
    const extraBuilds = {};

    if (target.scripts !== undefined) {
      Object.keys(target.scripts).forEach(key => {
        const script = target.scripts[key];
        const ma = script.match(/--output=([^\s]+)/);
        if (ma) {
          buildOutput = ma[1];
        }

        if (template.scripts !== undefined && key !== 'semantic-release') {
          const templateScript = template.scripts[key];
          if (templateScript !== undefined) {
            const mb = script.match(/&&\s*(.+)$/);
            if (mb && !templateScript.includes(mb[1])) {
              extraBuilds[key] = mb[1];
            }
          }
        }
      });
    }

    const usedDevModules = await context.usedDevModules();

    context.logger.debug(
      `usedDevModules: ${Array.from(usedDevModules).join(',')}`
    );

    const optionalDevModules = context.optionalDevModules(usedDevModules);

    context.logger.debug(
      `optionalDevModules: ${Array.from(optionalDevModules).join(',')}`
    );

    const deepPropeties = {
      scripts: {},
      devDependencies: {},
      engines: {}
    };

    Object.keys(deepPropeties).forEach(category => {
      if (target[category] === undefined) {
        target[category] = {};
      }

      if (template[category] !== undefined) {
        Object.keys(template[category]).forEach(d => {
          if (template[category][d] === '-') {
            if (target[category][d] !== undefined) {
              messages.push(
                `chore(${category}): remove ${d}@${target[category][d]}`
              );
              delete target[category][d];
            }
          } else {
            const tp = context.expand(template[category][d]);

            if (tp !== target[category][d]) {
              messages.push(
                target[category][d] === undefined
                  ? `chore(${category}): add ${d}@${tp} from template`
                  : `chore(${category}): update ${d}@${tp} from template`
              );
              target[category][d] = tp;
            }
          }
        });
      }
    });

    Object.keys(template).forEach(p => {
      if (p !== 'template') {
        if (target[p] === undefined) {
          target[p] = template[p];
        }
      }
    });

    if (target.scripts.prepare) {
      if (buildOutput !== undefined) {
        target.scripts.prepare = target.scripts.prepare.replace(
          /--output=([^\s]+)/,
          `--output=${buildOutput}`
        );
      }
    }

    Object.keys(extraBuilds).forEach(key => {
      target.scripts[key] += ` && ${extraBuilds[key]}`;
    });

    if (target.module === '{{module}}') {
      delete target.module;
    }

    if (
      target.contributors !== undefined &&
      target.author !== undefined &&
      target.author.name !== undefined
    ) {
      const m = target.author.name.match(/(^[^<]+)<([^>]+)>/);
      if (m !== undefined) {
        const name = String(m[1]).replace(/^\s+|\s+$/g, '');
        const email = m[2];

        if (
          target.contributors.find(c => c.name === name && c.email === email)
        ) {
          delete target.author;
        }
      }
    }

    if (
      target.template === undefined ||
      target.template.repository === undefined ||
      target.template.repository.url !==
        `https://github.com/${context.templateRepo.name}.git`
    ) {
      messages.push('chore(package): set template repo');

      target.template = {
        repository: {
          url: `https://github.com/${context.templateRepo.name}.git`
        }
      };
    }

    const toBeDeletedModules = Array.from(
      context.optionalDevModules(new Set(Object.keys(target.devDependencies)))
    ).filter(m => !usedDevModules.has(m));

    /*
    console.log(`modules: ${Object.keys(target.devDependencies)}`);
    console.log(`used modules: ${Array.from(usedDevModules)}`);
    console.log(
      `optional modules: ${Array.from(
        context.optionalDevModules(new Set(Object.keys(target.devDependencies)))
      )}`
    );
    console.log(`toBeDeletedModules: ${Array.from(toBeDeletedModules)}`);
*/

    toBeDeletedModules.forEach(d => {
      messages = messages.filter(
        m => !m.startsWith(`chore(devDependencies): add ${d}@`)
      );
      delete target.devDependencies[d];
    });

    target = deleter(target, template, messages, []);

    if (target.keywords !== undefined) {
      delete target.keywords['npm-package-template'];
    }

    if (
      template.template !== undefined &&
      template.template.keywords !== undefined
    ) {
      Object.keys(template.template.keywords).forEach(r =>
        addKeyword(
          target,
          new RegExp(r),
          template.template.keywords[r],
          messages
        )
      );
    }

    removeKeyword(target, ['null', null, undefined], messages);

    if (messages.length === 0) {
      messages.push('chore: update package.json from template');
    }

    let newContent = JSON.stringify(context.expand(target), undefined, 2);
    const lastChar = newContent[newContent.length - 1];

    // keep trailing newline
    if (originalLastChar === '\n' && lastChar === '}') {
      newContent += '\n';
    }

    return {
      content: newContent,
      messages,
      changed: original !== newContent
    };
  }
}

function deleter(object, reference, messages, path) {
  if (
    typeof object === 'string' ||
    object instanceof String ||
    object === true ||
    object === false ||
    object === undefined ||
    object === null ||
    typeof object === 'number' ||
    object instanceof Number
  ) {
    return object;
  }

  if (Array.isArray(object)) {
    return object.map((e, i) => {
      path.push(i);
      const n = deleter(
        object[i],
        Array.isArray(reference) ? reference[i] : undefined,
        messages,
        path
      );
      path.pop();
      return n;
    });
  }

  Object.keys(reference).forEach(key => {
    path.push(key);

    if (reference[key] === '--delete--' && object[key] !== undefined) {
      if (object[key] !== '--delete--') {
        messages.push(`chore(npm): delete ${path.join('.')}`);
      }
      delete object[key];
    } else {
      object[key] = deleter(object[key], reference[key], messages, path);
    }
    path.pop();
  });

  return object;
}

function removeKeyword(pkg, keywords, messages) {
  if (pkg.keywords !== undefined) {
    keywords.forEach(keyword => {
      if (pkg.keywords.find(k => k === keyword)) {
        messages.push(`docs(package): remove keyword ${keyword}`);
        pkg.keywords = pkg.keywords.filter(k => k !== keyword);
      }
    });

    if (pkg.keywords[0] === null || pkg.keywords[0] === undefined) {
      messages.push(`docs(package): remove keyword null`);
      pkg.keywords = [];
    }
  }
}

function addKeyword(pkg, regex, keyword, messages) {
  if (keyword === undefined || keyword === null || keyword === 'null') {
    return;
  }

  if (pkg.name.match(regex)) {
    if (pkg.keywords === undefined) {
      pkg.keywords = [];
    }
    if (!pkg.keywords.find(k => k === keyword)) {
      messages.push(`docs(package): add keyword ${keyword}`);
      pkg.keywords.push(keyword);
    }
  }
}
<commit_msg>fix(package): deleter() handle null references gracefully<commit_after>import File from './file';

function moduleNames(object) {
  if (object === undefined) return new Set();

  const modules = new Set();

  Object.keys(object).forEach(k => {
    const v = object[k];
    if (typeof v === 'string') {
      modules.add(v);
    } else if (Array.isArray(v)) {
      v.forEach(e => {
        if (typeof e === 'string') {
          modules.add(e);
        }
      });
    }
  });

  return modules;
}

export default class Package extends File {
  static matchesFileName(name) {
    return name.match(/^package\.json$/);
  }

  optionalDevModules(modules = new Set()) {
    return new Set(['cracks', 'dont-crack'].filter(m => modules.has(m)));
  }

  async usedDevModules(content) {
    content = await content;

    const pkg = content.length === 0 ? {} : JSON.parse(content);

    return moduleNames(pkg.release);
  }

  async templateRepo(context) {
    const content = await this.originalContent(context, {
      ignoreMissing: true
    });
    const pkg = JSON.parse(content);
    if (pkg.template !== undefined && pkg.template.repository !== undefined) {
      const m = pkg.template.repository.url.match(/github.com\/(.*)\.git$/);
      return m[1];
    }

    return undefined;
  }

  async mergeContent(context, original, templateContent) {
    const originalLastChar = original[original.length - 1];

    let target =
      original === undefined || original === '' ? {} : JSON.parse(original);
    const template = JSON.parse(templateContent);
    let messages = [];
    const properties = context.properties;

    if (target.name === undefined || target.name === '') {
      const m = context.targetRepo.name.match(/^([^\/]+)\/(.*)/);
      target.name = m ? m[2] : context.targetRepo.name;
    }

    if (target.module !== undefined && !target.module.match(/\{\{module\}\}/)) {
      properties.module = target.module;
    }

    properties.main =
      target.main && !target.main.match(/\{\{main\}\}/)
        ? target.main
        : 'dist/index.js';

    const githubURL = `git+https://github.com/${properties.user}/${target.name}.git`;
    const githubURLAlternative = `git+https://github.com/${properties.user}/node-${target.name}.git`;

    let repoName = target.name;

    if (
      target.repository === undefined ||
      (target.repository.url !== githubURL &&
        target.repository.url !== githubURLAlternative)
    ) {
      target.repository = {
        type: 'git',
        url: githubURL
      };
      messages.push(`chore(package): correct github url`);
    }

    if (
      target.repository !== undefined &&
      githubURLAlternative === target.repository.url
    ) {
      repoName = `node-${target.name}`;
    }

    const bugsURL = `https://github.com/${properties.user}/${repoName}/issues`;

    if (target.bugs === undefined || target.bugs.url !== bugsURL) {
      target.bugs = {
        url: bugsURL
      };
      messages.push(`chore(package): correct bugs url`);
    }

    const homepageURL = `https://github.com/${properties.user}/${repoName}#readme`;

    if (target.homepage === undefined || target.homepage !== homepageURL) {
      target.homepage = homepageURL;
      messages.push(`chore(package): correct hompage url`);
    }

    let buildOutput;
    const extraBuilds = {};

    if (target.scripts !== undefined) {
      Object.keys(target.scripts).forEach(key => {
        const script = target.scripts[key];
        const ma = script.match(/--output=([^\s]+)/);
        if (ma) {
          buildOutput = ma[1];
        }

        if (template.scripts !== undefined && key !== 'semantic-release') {
          const templateScript = template.scripts[key];
          if (templateScript !== undefined) {
            const mb = script.match(/&&\s*(.+)$/);
            if (mb && !templateScript.includes(mb[1])) {
              extraBuilds[key] = mb[1];
            }
          }
        }
      });
    }

    const usedDevModules = await context.usedDevModules();

    context.logger.debug(
      `usedDevModules: ${Array.from(usedDevModules).join(',')}`
    );

    const optionalDevModules = context.optionalDevModules(usedDevModules);

    context.logger.debug(
      `optionalDevModules: ${Array.from(optionalDevModules).join(',')}`
    );

    const deepPropeties = {
      scripts: {},
      devDependencies: {},
      engines: {}
    };

    Object.keys(deepPropeties).forEach(category => {
      if (target[category] === undefined) {
        target[category] = {};
      }

      if (template[category] !== undefined) {
        Object.keys(template[category]).forEach(d => {
          if (template[category][d] === '-') {
            if (target[category][d] !== undefined) {
              messages.push(
                `chore(${category}): remove ${d}@${target[category][d]}`
              );
              delete target[category][d];
            }
          } else {
            const tp = context.expand(template[category][d]);

            if (tp !== target[category][d]) {
              messages.push(
                target[category][d] === undefined
                  ? `chore(${category}): add ${d}@${tp} from template`
                  : `chore(${category}): update ${d}@${tp} from template`
              );
              target[category][d] = tp;
            }
          }
        });
      }
    });

    Object.keys(template).forEach(p => {
      if (p !== 'template') {
        if (target[p] === undefined) {
          target[p] = template[p];
        }
      }
    });

    if (target.scripts.prepare) {
      if (buildOutput !== undefined) {
        target.scripts.prepare = target.scripts.prepare.replace(
          /--output=([^\s]+)/,
          `--output=${buildOutput}`
        );
      }
    }

    Object.keys(extraBuilds).forEach(key => {
      target.scripts[key] += ` && ${extraBuilds[key]}`;
    });

    if (target.module === '{{module}}') {
      delete target.module;
    }

    if (
      target.contributors !== undefined &&
      target.author !== undefined &&
      target.author.name !== undefined
    ) {
      const m = target.author.name.match(/(^[^<]+)<([^>]+)>/);
      if (m !== undefined) {
        const name = String(m[1]).replace(/^\s+|\s+$/g, '');
        const email = m[2];

        if (
          target.contributors.find(c => c.name === name && c.email === email)
        ) {
          delete target.author;
        }
      }
    }

    if (
      target.template === undefined ||
      target.template.repository === undefined ||
      target.template.repository.url !==
        `https://github.com/${context.templateRepo.name}.git`
    ) {
      messages.push('chore(package): set template repo');

      target.template = {
        repository: {
          url: `https://github.com/${context.templateRepo.name}.git`
        }
      };
    }

    const toBeDeletedModules = Array.from(
      context.optionalDevModules(new Set(Object.keys(target.devDependencies)))
    ).filter(m => !usedDevModules.has(m));

    /*
    console.log(`modules: ${Object.keys(target.devDependencies)}`);
    console.log(`used modules: ${Array.from(usedDevModules)}`);
    console.log(
      `optional modules: ${Array.from(
        context.optionalDevModules(new Set(Object.keys(target.devDependencies)))
      )}`
    );
    console.log(`toBeDeletedModules: ${Array.from(toBeDeletedModules)}`);
*/

    toBeDeletedModules.forEach(d => {
      messages = messages.filter(
        m => !m.startsWith(`chore(devDependencies): add ${d}@`)
      );
      delete target.devDependencies[d];
    });

    target = deleter(target, template, messages, []);

    if (target.keywords !== undefined) {
      delete target.keywords['npm-package-template'];
    }

    if (
      template.template !== undefined &&
      template.template.keywords !== undefined
    ) {
      Object.keys(template.template.keywords).forEach(r =>
        addKeyword(
          target,
          new RegExp(r),
          template.template.keywords[r],
          messages
        )
      );
    }

    removeKeyword(target, ['null', null, undefined], messages);

    if (messages.length === 0) {
      messages.push('chore: update package.json from template');
    }

    let newContent = JSON.stringify(context.expand(target), undefined, 2);
    const lastChar = newContent[newContent.length - 1];

    // keep trailing newline
    if (originalLastChar === '\n' && lastChar === '}') {
      newContent += '\n';
    }

    return {
      content: newContent,
      messages,
      changed: original !== newContent
    };
  }
}

function deleter(object, reference, messages, path) {
  if (
    typeof object === 'string' ||
    object instanceof String ||
    object === true ||
    object === false ||
    object === undefined ||
    object === null ||
    typeof object === 'number' ||
    object instanceof Number
  ) {
    return object;
  }

  if (Array.isArray(object)) {
    return object.map((e, i) => {
      path.push(i);
      const n = deleter(
        object[i],
        Array.isArray(reference) ? reference[i] : undefined,
        messages,
        path
      );
      path.pop();
      return n;
    });
  }

  if (reference) {
    Object.keys(reference).forEach(key => {
      path.push(key);

      if (reference[key] === '--delete--' && object[key] !== undefined) {
        if (object[key] !== '--delete--') {
          messages.push(`chore(npm): delete ${path.join('.')}`);
        }
        delete object[key];
      } else {
        object[key] = deleter(object[key], reference[key], messages, path);
      }
      path.pop();
    });
  }

  return object;
}

function removeKeyword(pkg, keywords, messages) {
  if (pkg.keywords !== undefined) {
    keywords.forEach(keyword => {
      if (pkg.keywords.find(k => k === keyword)) {
        messages.push(`docs(package): remove keyword ${keyword}`);
        pkg.keywords = pkg.keywords.filter(k => k !== keyword);
      }
    });

    if (pkg.keywords[0] === null || pkg.keywords[0] === undefined) {
      messages.push(`docs(package): remove keyword null`);
      pkg.keywords = [];
    }
  }
}

function addKeyword(pkg, regex, keyword, messages) {
  if (keyword === undefined || keyword === null || keyword === 'null') {
    return;
  }

  if (pkg.name.match(regex)) {
    if (pkg.keywords === undefined) {
      pkg.keywords = [];
    }
    if (!pkg.keywords.find(k => k === keyword)) {
      messages.push(`docs(package): add keyword ${keyword}`);
      pkg.keywords.push(keyword);
    }
  }
}
<|endoftext|>


<commit_before>'use strict';

/*!
 * Snakeskin
 * https://github.com/SnakeskinTpl/Snakeskin
 *
 * Released under the MIT license
 * https://github.com/SnakeskinTpl/Snakeskin/blob/master/LICENSE
 */

import Snakeskin from '../core';
import { any } from '../helpers/gcc';
import { isArray } from '../helpers/types';

/**
 * Special Snakeskin class for escaping HTML entities from an object
 *
 * @constructor
 * @param {?} obj - source object
 * @param {?string=} [opt_attr] - type of attribute declaration
 */
Snakeskin.HTMLObject = function (obj, opt_attr) {
	this.value = obj;
	this.attr = opt_attr;
};

/**
 * StringBuffer constructor
 *
 * @constructor
 * @return {!Array}
 */
Snakeskin.StringBuffer = function () {
	return [];
};

/**
 * @param {!Function} child
 * @param {!Function} parent
 */
function inherit(child, parent) {
	const F = function () {
		this.constructor = child;
	};

	F.prototype = parent.prototype;
	child.prototype = new F();
}

/**
 * Node constructor
 * @constructor
 */
Snakeskin.Node = function () {};

/**
 * Returns the number of child elements
 * @return {number}
 */
Snakeskin.Node.prototype.length = function () {
	return this.value.childNodes.length;
};

/**
 * Returns text content
 * @return {number}
 */
Snakeskin.Node.prototype.textContent = function () {
	return this.value.textContent;
};

/**
 * DocumentFragment constructor
 *
 * @constructor
 * @extends {Snakeskin.Node}
 * @param {string} renderMode - rendering mode of templates
 */
Snakeskin.DocumentFragment = function (renderMode) {
	this.renderMode = renderMode;
	this.value = document.createDocumentFragment();
};

inherit(Snakeskin.DocumentFragment, Snakeskin.Node);

/**
 * Appends a child to the document fragment
 * @param {?} el - element for appending
 */
Snakeskin.DocumentFragment.prototype.appendChild = function (el) {
	this.value.appendChild(el);
};

/**
 * Element constructor
 *
 * @constructor
 * @extends {Snakeskin.Node}
 *
 * @param {string} name - element name
 * @param {string} renderMode - rendering mode of templates
 */
Snakeskin.Element = function (name, renderMode) {
	this.renderMode = renderMode;
	this.value = document.createElement(name);
};

inherit(Snakeskin.Element, Snakeskin.Node);

/**
 * Appends a child to the element
 * @param {?} el - element for appending
 */
Snakeskin.Element.prototype.appendChild = function (el) {
	this.value.appendChild(el);
};

/**
 * Sets an attribute to the element
 *
 * @param {string} name - attribute name
 * @param {string} val - attribute value
 */
Snakeskin.Element.prototype.setAttribute = function (name, val) {
	this.value.setAttribute(name, val);
};

/**
 * Comment constructor
 *
 * @constructor
 * @extends {Snakeskin.Node}
 *
 * @param {string} text - comment text
 * @param {string} renderMode - rendering mode of templates
 */
Snakeskin.Comment = function (text, renderMode) {
	this.renderMode = renderMode;
	this.value = document.createComment(text);
};

inherit(Snakeskin.Comment, Snakeskin.Node);

/**
 * Text constructor
 *
 * @constructor
 * @extends {Snakeskin.Node}
 *
 * @param {string} text
 * @param {string} renderMode - rendering mode of templates
 */
Snakeskin.Text = function (text, renderMode) {
	this.renderMode = renderMode;
	this.value = document.createTextNode(text);
};

inherit(Snakeskin.Text, Snakeskin.Node);

/**
 * Map of inline tag names
 * @const
 */
Snakeskin.inlineTags = {
	'area': 'href',
	'base': 'href',
	'br': true,
	'col': true,
	'embed': 'src',
	'hr': true,
	'img': 'src',
	'input': 'value',
	'link': 'href',
	'meta': 'content',
	'param': 'value',
	'source': 'src',
	'track': 'src',
	'wbr': true
};

/**
 * Appends a value to the specified element
 *
 * @param {(!Snakeskin.DocumentFragment|!Snakeskin.Element)} el - base element
 * @param {?} val - value for appending
 * @param {string} renderMode - rendering mode of templates
 * @return {(!Snakeskin.Element|!Snakeskin.Comment|!Snakeskin.Text)}
 */
Snakeskin.appendChild = function (el, val, renderMode) {
	if (val instanceof Snakeskin.Node === false) {
		val = Snakeskin.Text(String(val), renderMode);
	}

	el.appendChild(val.value);
	return val;
};

/**
 * Sets an attribute to the specified element
 *
 * @param {!Snakeskin.Node} node - source element
 * @param {string} name - attribute name
 * @param {?} val - attribute value
 */
Snakeskin.setAttribute = function (node, name, val) {
	node.setAttribute(name, val instanceof Snakeskin.Node ? val.textContent : String(val));
};

const
	keys = (() => /\[native code]/.test(Object.keys && Object.keys.toString()) && Object.keys)();

/**
 * Common iterator
 * (with hasOwnProperty for objects)
 *
 * @param {(Array|Object|undefined)} obj - source object
 * @param {(
 *   function(?, number, !Array, boolean, boolean, number)|
 *   function(?, string, !Object, number, boolean, boolean, number)
 * )} callback - callback function
 */
Snakeskin.forEach = function (obj, callback) {
	if (!obj) {
		return;
	}

	let
		length = 0;

	if (isArray(obj)) {
		length = obj.length;
		for (let i = -1; ++i < length;) {
			if (callback(obj[i], i, obj, i === 0, i === length - 1, length) === false) {
				break;
			}
		}

	} else if (keys) {
		const
			arr = keys(obj);

		length = arr.length;
		for (let i = -1; ++i < length;) {
			if (callback(obj[arr[i]], arr[i], obj, i, i === 0, i === length - 1, length) === false) {
				break;
			}
		}

	} else {
		if (callback.length >= 6) {
			for (let key in obj) {
				if (!obj.hasOwnProperty(key)) {
					continue;
				}

				length++;
			}
		}

		let i = 0;
		for (let key in obj) {
			if (!obj.hasOwnProperty(key)) {
				continue;
			}

			if (callback(obj[key], key, obj, i, i === 0, i === length - 1, length) === false) {
				break;
			}

			i++;
		}
	}
};

/**
 * Object iterator
 * (without hasOwnProperty)
 *
 * @param {(Object|undefined)} obj - source object
 * @param {function(?, string, !Object, number, boolean, boolean, number)} callback - callback function
 */
Snakeskin.forIn = function (obj, callback) {
	if (!obj) {
		return;
	}

	let
		length = 0,
		i = 0;

	if (callback.length >= 6) {
		for (let ignore in obj) {
			length++;
		}
	}

	for (let key in obj) {
		if (callback(obj[key], key, obj, i, i === 0, i === length - 1, length) === false) {
			break;
		}

		i++;
	}
};

/**
 * Decorates a function by another functions
 *
 * @param {!Array<!Function>} decorators - array of decorator functions
 * @param {!Function} fn - source function
 * @return {!Function}
 */
Snakeskin.decorate = function (decorators, fn) {
	Snakeskin.forEach(decorators, (decorator) => fn = decorator(fn));
	fn.decorators = decorators;
	return fn;
};
<commit_msg>:wrench:<commit_after>'use strict';

/*!
 * Snakeskin
 * https://github.com/SnakeskinTpl/Snakeskin
 *
 * Released under the MIT license
 * https://github.com/SnakeskinTpl/Snakeskin/blob/master/LICENSE
 */

import Snakeskin from '../core';
import { any } from '../helpers/gcc';
import { isArray } from '../helpers/types';

/**
 * Special Snakeskin class for escaping HTML entities from an object
 *
 * @constructor
 * @param {?} obj - source object
 * @param {?string=} [opt_attr] - type of attribute declaration
 */
Snakeskin.HTMLObject = function (obj, opt_attr) {
	this.value = obj;
	this.attr = opt_attr;
};

/**
 * StringBuffer constructor
 *
 * @constructor
 * @return {!Array}
 */
Snakeskin.StringBuffer = function () {
	return [];
};

/**
 * @param {!Function} child
 * @param {!Function} parent
 */
function inherit(child, parent) {
	const F = function () {
		this.constructor = child;
	};

	F.prototype = parent.prototype;
	child.prototype = new F();
}

/**
 * Node constructor
 * @constructor
 */
Snakeskin.Node = function () {};

/**
 * Returns the number of child elements
 * @return {number}
 */
Snakeskin.Node.prototype.length = function () {
	return this.value.childNodes.length;
};

/**
 * Returns text content
 * @return {number}
 */
Snakeskin.Node.prototype.textContent = function () {
	return this.value.textContent;
};

/**
 * DocumentFragment constructor
 *
 * @constructor
 * @extends {Snakeskin.Node}
 * @param {string} renderMode - rendering mode of templates
 */
Snakeskin.DocumentFragment = function (renderMode) {
	this.renderMode = renderMode;
	this.value = document.createDocumentFragment();
};

inherit(Snakeskin.DocumentFragment, Snakeskin.Node);

/**
 * Appends a child to the document fragment
 * @param {?} el - element for appending
 */
Snakeskin.DocumentFragment.prototype.appendChild = function (el) {
	this.value.appendChild(el);
};

/**
 * Element constructor
 *
 * @constructor
 * @extends {Snakeskin.Node}
 *
 * @param {string} name - element name
 * @param {string} renderMode - rendering mode of templates
 */
Snakeskin.Element = function (name, renderMode) {
	this.renderMode = renderMode;
	this.value = document.createElement(name);
};

inherit(Snakeskin.Element, Snakeskin.Node);

/**
 * Appends a child to the element
 * @param {?} el - element for appending
 */
Snakeskin.Element.prototype.appendChild = function (el) {
	this.value.appendChild(el);
};

/**
 * Sets an attribute to the element
 *
 * @param {string} name - attribute name
 * @param {string} val - attribute value
 */
Snakeskin.Element.prototype.setAttribute = function (name, val) {
	this.value.setAttribute(name, val);
};

/**
 * Comment constructor
 *
 * @constructor
 * @extends {Snakeskin.Node}
 *
 * @param {string} text - comment text
 * @param {string} renderMode - rendering mode of templates
 */
Snakeskin.Comment = function (text, renderMode) {
	this.renderMode = renderMode;
	this.value = document.createComment(text);
};

inherit(Snakeskin.Comment, Snakeskin.Node);

/**
 * Text constructor
 *
 * @constructor
 * @extends {Snakeskin.Node}
 *
 * @param {string} text
 * @param {string} renderMode - rendering mode of templates
 */
Snakeskin.Text = function (text, renderMode) {
	this.renderMode = renderMode;
	this.value = document.createTextNode(text);
};

inherit(Snakeskin.Text, Snakeskin.Node);

/**
 * Map of inline tag names
 * @const
 */
Snakeskin.inlineTags = {
	'area': 'href',
	'base': 'href',
	'br': true,
	'col': true,
	'embed': 'src',
	'hr': true,
	'img': 'src',
	'input': 'value',
	'link': 'href',
	'meta': 'content',
	'param': 'value',
	'source': 'src',
	'track': 'src',
	'wbr': true
};

/**
 * Appends a value to the specified element
 *
 * @param {(!Snakeskin.DocumentFragment|!Snakeskin.Element)} el - base element
 * @param {?} val - value for appending
 * @param {string} renderMode - rendering mode of templates
 * @return {(!Snakeskin.Element|!Snakeskin.Comment|!Snakeskin.Text)}
 */
Snakeskin.appendChild = function (el, val, renderMode) {
	if (val instanceof Snakeskin.Node === false) {
		val = new Snakeskin.Text(String(val), renderMode);
	}

	el.appendChild(val.value);
	return val;
};

/**
 * Sets an attribute to the specified element
 *
 * @param {!Snakeskin.Node} node - source element
 * @param {string} name - attribute name
 * @param {?} val - attribute value
 */
Snakeskin.setAttribute = function (node, name, val) {
	node.setAttribute(name, val instanceof Snakeskin.Node ? val.textContent() : String(val));
};

const
	keys = (() => /\[native code]/.test(Object.keys && Object.keys.toString()) && Object.keys)();

/**
 * Common iterator
 * (with hasOwnProperty for objects)
 *
 * @param {(Array|Object|undefined)} obj - source object
 * @param {(
 *   function(?, number, !Array, boolean, boolean, number)|
 *   function(?, string, !Object, number, boolean, boolean, number)
 * )} callback - callback function
 */
Snakeskin.forEach = function (obj, callback) {
	if (!obj) {
		return;
	}

	let
		length = 0;

	if (isArray(obj)) {
		length = obj.length;
		for (let i = -1; ++i < length;) {
			if (callback(obj[i], i, obj, i === 0, i === length - 1, length) === false) {
				break;
			}
		}

	} else if (keys) {
		const
			arr = keys(obj);

		length = arr.length;
		for (let i = -1; ++i < length;) {
			if (callback(obj[arr[i]], arr[i], obj, i, i === 0, i === length - 1, length) === false) {
				break;
			}
		}

	} else {
		if (callback.length >= 6) {
			for (let key in obj) {
				if (!obj.hasOwnProperty(key)) {
					continue;
				}

				length++;
			}
		}

		let i = 0;
		for (let key in obj) {
			if (!obj.hasOwnProperty(key)) {
				continue;
			}

			if (callback(obj[key], key, obj, i, i === 0, i === length - 1, length) === false) {
				break;
			}

			i++;
		}
	}
};

/**
 * Object iterator
 * (without hasOwnProperty)
 *
 * @param {(Object|undefined)} obj - source object
 * @param {function(?, string, !Object, number, boolean, boolean, number)} callback - callback function
 */
Snakeskin.forIn = function (obj, callback) {
	if (!obj) {
		return;
	}

	let
		length = 0,
		i = 0;

	if (callback.length >= 6) {
		for (let ignore in obj) {
			length++;
		}
	}

	for (let key in obj) {
		if (callback(obj[key], key, obj, i, i === 0, i === length - 1, length) === false) {
			break;
		}

		i++;
	}
};

/**
 * Decorates a function by another functions
 *
 * @param {!Array<!Function>} decorators - array of decorator functions
 * @param {!Function} fn - source function
 * @return {!Function}
 */
Snakeskin.decorate = function (decorators, fn) {
	Snakeskin.forEach(decorators, (decorator) => fn = decorator(fn));
	fn.decorators = decorators;
	return fn;
};
<|endoftext|>