text: "use strict";
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
      if (k2 === undefined) k2 = k;
      Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
  }) : (function(o, m, k, k2) {
      if (k2 === undefined) k2 = k;
      o[k2] = m[k];
  }));
  var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
      Object.defineProperty(o, "default", { enumerable: true, value: v });
  }) : function(o, v) {
      o["default"] = v;
  });
  var __importStar = (this && this.__importStar) || function (mod) {
      if (mod && mod.__esModule) return mod;
      var result = {};
      if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
      __setModuleDefault(result, mod);
      return result;
  };
  var __importDefault = (this && this.__importDefault) || function (mod) {
      return (mod && mod.__esModule) ? mod : { "default": mod };
  };
  Object.defineProperty(exports, "__esModule", { value: true });
  exports.mergeFiles = exports.WarningInfo = exports.makeCachedProjectSyncDataProvider = exports.ProjectSyncMetadataModel = exports.ComponentSkeletonModel = exports.renameAndSerializePlasmicASTNode = void 0;
  const babel = __importStar(require("@babel/core"));
  const parser = __importStar(require("@babel/parser"));
  const traverse_1 = __importDefault(require("@babel/traverse"));
  const diff_1 = require("diff");
  const L = __importStar(require("lodash"));
  const lodash_1 = require("lodash");
  const cloneDeepWithHook_1 = require("./cloneDeepWithHook");
  const common_1 = require("./common");
  const plasmic_ast_1 = require("./plasmic-ast");
  const plasmic_parser_1 = require("./plasmic-parser");
  const utils_1 = require("./utils");
  const mkJsxFragment = (children) => {
      return babel.types.jsxFragment(babel.types.jsxOpeningFragment(), babel.types.jsxClosingFragment(), 
      // Remove unnecessary {}
      children.map((child) => child.type === "JSXExpressionContainer" &&
          child.expression.type === "JSXElement"
          ? child.expression
          : child));
  };
  const getRawNamedAttrs = (jsxElement) => {
      const attrs = new Map();
      for (const attr of jsxElement.openingElement.attributes) {
          if (attr.type !== "JSXAttribute") {
              continue;
          }
          common_1.assert(L.isString(attr.name.name));
          attrs.set(attr.name.name, attr);
      }
      return attrs;
  };
  const findParsedNamedAttrs = (node, name) => {
      for (const attr of node.jsxElement.attrs) {
          if (!L.isArray(attr)) {
              continue;
          }
          if (attr[0] === name) {
              return attr[1];
          }
      }
      return undefined;
  };
  const mergedTag = (newNode, editedNode, baseNode) => {
      const editedTag = editedNode.jsxElement.rawNode.openingElement.name;
      const newTag = newNode.jsxElement.rawNode.openingElement.name;
      const baseTagCode = utils_1.tagName(baseNode.jsxElement.rawNode);
      const editedTagCode = utils_1.tagName(editedNode.jsxElement.rawNode);
      if (baseTagCode === editedTagCode) {
          return newTag;
      }
      const newTagCode = utils_1.tagName(newNode.jsxElement.rawNode);
      // User edited the tag.
      if (baseTagCode === newTagCode || editedTagCode === newTagCode) {
          return editedTag;
      }
      // Generate a bad tag identifier for user to resolve conflicts
      return babel.types.jsxIdentifier(`${editedTagCode}___${newTagCode}`);
  };
  const wrapAsJsxAttrValue = (rawValue) => {
      if (!rawValue) {
          return undefined;
      }
      if (rawValue.type === "JSXText" || rawValue.type === "JSXSpreadChild") {
          // Either newVersion or editedVersion was wrapped in a fragment.
          // We wrap too.
          mkJsxFragment([rawValue]);
      }
      else if (babel.types.isExpression(rawValue) ||
          babel.types.isJSXEmptyExpression(rawValue)) {
          return babel.types.jsxExpressionContainer(rawValue);
      }
      else {
          return rawValue;
      }
  };
  const mergeAttributeValue = (attrName, newNode, editedNode, baseNode, codeVersions) => {
      const parsedNewAttrValue = findParsedNamedAttrs(newNode, attrName);
      const parsedEditedAttrValue = findParsedNamedAttrs(editedNode, attrName);
      const parsedBaseAttrValue = findParsedNamedAttrs(baseNode, attrName);
      // These two must not be undefined (i.e. not found)
      common_1.assert(parsedNewAttrValue !== undefined);
      common_1.assert(parsedEditedAttrValue !== undefined);
      let mergedAttrValue = undefined;
      if (!parsedNewAttrValue) {
          if (parsedEditedAttrValue) {
              mergedAttrValue = serializePlasmicASTNode(parsedEditedAttrValue, codeVersions);
          }
          else {
              mergedAttrValue = undefined;
          }
      }
      else {
          if (!parsedEditedAttrValue) {
              mergedAttrValue = serializePlasmicASTNode(parsedNewAttrValue, codeVersions);
          }
          else {
              // do merge, really!
              const asArray = (node) => !node ? [] : node.type === "jsx-fragment" ? node.children : [node];
              const mergedNodes = mergeNodes(asArray(parsedNewAttrValue), asArray(parsedEditedAttrValue), asArray(parsedBaseAttrValue), codeVersions);
              // If edited forced wrapping single node with JSXFragment, so do we
              // if the result is a single element.
              const editedForcedFragment = parsedEditedAttrValue.type === "jsx-fragment" &&
                  parsedEditedAttrValue.children.length === 1;
              mergedAttrValue =
                  mergedNodes.length > 1
                      ? mkJsxFragment(mergedNodes)
                      : mergedNodes.length === 1
                          ? editedForcedFragment
                              ? mkJsxFragment(mergedNodes)
                              : mergedNodes[0]
                          : babel.types.jsxEmptyExpression();
          }
      }
      return wrapAsJsxAttrValue(mergedAttrValue);
  };
  const serializeNamedAttribute = (attrName, rawValue, codeVersions) => {
      const attrValue = rawValue
          ? serializePlasmicASTNode(rawValue, codeVersions)
          : undefined;
      return babel.types.jsxAttribute(attrName, wrapAsJsxAttrValue(attrValue));
  };
  const serializeJsxSpreadAttribute = (editedAttr, newNodeHasPropsWithIdSpreador, nodeId, codeVersions) => {
      if (!newNodeHasPropsWithIdSpreador &&
          plasmic_parser_1.isCallWithoutArguments(editedAttr.rawNode, plasmic_parser_1.helperObject, `props${nodeId}`)) {
          // delete the id spreador, if deleted from new version
          return undefined;
      }
      const attrValue = serializePlasmicASTNode(editedAttr, codeVersions);
      if (!attrValue || attrValue.type === "JSXEmptyExpression") {
          return undefined;
      }
      common_1.assert(attrValue.type !== "JSXText");
      common_1.assert(attrValue.type !== "JSXSpreadChild");
      if (babel.types.isExpression(attrValue)) {
          return babel.types.jsxSpreadAttribute(attrValue);
      }
      else if (attrValue.type === "JSXExpressionContainer") {
          return attrValue.expression.type === "JSXEmptyExpression"
              ? undefined
              : babel.types.jsxSpreadAttribute(attrValue.expression);
      }
  };
  const mergeAttributes = (newNode, editedNode, baseNode, codeVersions) => {
      const { newVersion, editedVersion } = codeVersions;
      common_1.assert(editedNode.jsxElement.nameInId === newNode.jsxElement.nameInId);
      common_1.assert(editedNode.jsxElement.nameInId === baseNode.jsxElement.nameInId);
      const newNodePropsWithIdSpreador = newVersion.tryGetPropsIdSpreador(newNode);
      const editedHasPropsWithIdSpreador = editedVersion.hasPropsIdSpreador(editedNode);
      const newNamedAttrs = getRawNamedAttrs(newNode.jsxElement.rawNode);
      const editedNamedAttrs = getRawNamedAttrs(editedNode.jsxElement.rawNode);
      const baseNamedAttrs = getRawNamedAttrs(baseNode.jsxElement.rawNode);
      const conflictResolution = (name, baseAttr, editedAttr, newAttr) => {
          // If attribute match, then emit either version is ok. Emitting it at the
          // place where the edited version emitted at is probably safer, and less
          // disturbing.
          if (utils_1.nodesDeepEqualIgnoreComments(editedAttr, newAttr)) {
              return "emit-edited";
          }
          if (!baseAttr) {
              // We don't know how to handle the conflict. Merge them and let developer
              // handle it.
              return "emit-merged";
          }
          if (utils_1.nodesDeepEqualIgnoreComments(baseAttr, editedAttr)) {
              // User didn't edit it. Emit the new version.
              return "emit-new";
          }
          if (name.startsWith("on") ||
              (name === "value" &&
                  utils_1.tagName(newNode.jsxElement.rawNode) === "PlasmicSlot") ||
              utils_1.nodesDeepEqualIgnoreComments(baseAttr, newAttr)) {
              // Plasmic doesn't change it. Emit the edited version then.
              return "emit-edited";
          }
          // Both Plasmic and developer edited it. Emit both.
          return "emit-merged";
      };
      const emitAttrInEditedNode = (attrName) => {
          const editedAttr = common_1.ensure(editedNamedAttrs.get(attrName));
          const newAttr = newNamedAttrs.get(attrName);
          const baseAttr = baseNamedAttrs.get(attrName);
          if (newAttr) {
              const res = conflictResolution(attrName, baseAttr, editedAttr, newAttr);
              if (res === "emit-new") {
                  // We emit the newAttr in place to minimize diff.
                  return newAttr;
              }
              else if (res === "emit-merged") {
                  const value = mergeAttributeValue(attrName, newNode, editedNode, baseNode, codeVersions);
                  return babel.types.jsxAttribute(newAttr.name, value);
              }
              common_1.assert(res === "emit-edited");
              return editedAttr;
          }
          else if (!baseAttr) {
              // user added attribute in edited version. Emit the edited attribute
              // without any transformation.
              return serializeNamedAttribute(editedAttr.name, findParsedNamedAttrs(editedNode, attrName), codeVersions);
          }
          else {
              // Attribute deleted in new version. However, user may have modified it.
              // Delete it only if there is no modification; otherwise, keep it for user
              // to fix the compilation failure.
              return utils_1.nodesDeepEqualIgnoreComments(baseAttr, editedAttr)
                  ? undefined
                  : serializeNamedAttribute(editedAttr.name, findParsedNamedAttrs(editedNode, attrName), codeVersions);
          }
      };
      const mergedAttrs = [];
      newNamedAttrs.forEach((attr, name) => {
          const editedAttr = editedNamedAttrs.get(name);
          const baseAttr = baseNamedAttrs.get(name);
          if (!baseAttr && !editedAttr) {
              // newly added attribute in new version
              mergedAttrs.push(serializeNamedAttribute(attr.name, findParsedNamedAttrs(newNode, name), codeVersions));
          }
      });
      for (const attrInEditedNode of editedNode.jsxElement.attrs) {
          if (L.isArray(attrInEditedNode)) {
              const toEmit = emitAttrInEditedNode(attrInEditedNode[0]);
              if (toEmit) {
                  mergedAttrs.push(toEmit);
              }
          }
          else {
              const serializedSpreador = serializeJsxSpreadAttribute(attrInEditedNode, !!newNodePropsWithIdSpreador, newNode.jsxElement.nameInId, codeVersions);
              if (serializedSpreador) {
                  mergedAttrs.push(serializedSpreador);
              }
          }
      }
      let classNameAt = mergedAttrs.findIndex((attr) => utils_1.isAttribute(attr, "className"));
      // insert className if missing in edited version, mostly to support old code.
      if (classNameAt === -1) {
          const newClassNameAttr = newNamedAttrs.get("className");
          if (newClassNameAttr) {
              mergedAttrs.splice(0, 0, newClassNameAttr);
              classNameAt = 0;
          }
      }
      if (newNodePropsWithIdSpreador && !editedHasPropsWithIdSpreador) {
          // insert the new spreador right after className if any, always
          const insertSpreadorAt = classNameAt === -1 ? 0 : classNameAt + 1;
          mergedAttrs.splice(insertSpreadorAt, 0, newNodePropsWithIdSpreador);
      }
      return mergedAttrs;
  };
  const computeDiff = (s1, s2) => {
      return L.sum(diff_1.diffChars(s1, s2).map((part) => part.value.length));
  };
  const findMatch = (nodes, start, n) => {
      let matchingTypeAt = -1;
      if (n.type === "text" || n.type === "string-lit") {
          for (let i = start; i < nodes.length; i++) {
              const ni = nodes[i];
              let lastDiff = 0;
              if (ni.type === "text" || ni.type === "string-lit") {
                  if (ni.value === n.value) {
                      return { type: "perfect", index: i };
                  }
                  const diff = computeDiff(n.value, ni.value);
                  // Find the best match, i.e. the one with least diff.
                  if (matchingTypeAt === -1 || diff < lastDiff) {
                      matchingTypeAt = i;
                      lastDiff = diff;
                  }
              }
          }
      }
      else if (n.type === "child-str-call") {
          for (let i = start; i < nodes.length; i++) {
              const ni = nodes[i];
              if (ni.type === "child-str-call") {
                  return { type: "perfect", index: i };
              }
              if (matchingTypeAt === -1 &&
                  (ni.type === "text" || ni.type === "string-lit")) {
                  matchingTypeAt = i;
              }
          }
      }
      else if (n.type === "tag-or-component") {
          for (let i = start; i < nodes.length; i++) {
              const ni = nodes[i];
              if (ni.type === "tag-or-component" &&
                  ni.jsxElement.nameInId === n.jsxElement.nameInId) {
                  return { type: "perfect", index: i };
              }
              if (matchingTypeAt === -1 && ni.type === "tag-or-component") {
                  matchingTypeAt = i;
              }
          }
      }
      return matchingTypeAt !== -1
          ? { type: "type", index: matchingTypeAt }
          : { type: "none" };
  };
  const mergeNodes = (newNodes, editedNodes, baseNodes, codeVersions) => {
      let nextInsertStartAt = 0;
      const insertEditedNode = (editedChild, prevEditedChild) => {
          if (!prevEditedChild) {
              merged.splice(0, 0, editedChild);
              nextInsertStartAt = 1;
          }
          else {
              const prevMatch = findMatch(merged, nextInsertStartAt, prevEditedChild);
              if (prevMatch.type === "perfect" || prevMatch.type === "type") {
                  // previous node matches merged[prevMatch]. insert current node at
                  // prevMatch + 1.
                  merged.splice(prevMatch.index + 1, 0, editedChild);
                  nextInsertStartAt = prevMatch.index + 2;
              }
              else {
                  merged.splice(nextInsertStartAt, 0, editedChild);
                  nextInsertStartAt += 1;
              }
          }
      };
      // remove tag node from new nodes list when there is no perfect match.
      const merged = newNodes.filter((newNode) => {
          if (newNode.type === "tag-or-component") {
              const matchInEditedVersion = findMatch(editedNodes, 0, newNode);
              const matchInBaseVersion = findMatch(baseNodes, 0, newNode);
              if (matchInBaseVersion.type === "perfect" &&
                  matchInEditedVersion.type !== "perfect") {
                  return false;
              }
          }
          return true;
      });
      editedNodes.forEach((editedChild, i) => {
          if (editedChild.type === "text" ||
              editedChild.type === "string-lit" ||
              editedChild.type === "child-str-call") {
              const matchInNewVersion = findMatch(merged, nextInsertStartAt, editedChild);
              if (matchInNewVersion.type === "perfect") {
                  // skip text node if it matches some text in new version. But make sure
                  // subsequent nodes are inserted after the match.
                  nextInsertStartAt = matchInNewVersion.index + 1;
                  return;
              }
              const matchInBaseVersion = findMatch(baseNodes, 0, editedChild);
              if (matchInBaseVersion.type === "perfect") {
                  // skip text node if it matches some text in base version
                  return;
              }
              if (matchInBaseVersion.type === "type") {
                  const matchInMergedVersion = findMatch(merged, nextInsertStartAt, baseNodes[matchInBaseVersion.index]);
                  if (matchInMergedVersion.type === "perfect") {
                      // If base and new/merged version has a perfect match, then replace new version
                      // with the edited version.
                      merged.splice(matchInMergedVersion.index, 1, editedChild);
                      return;
                  }
              }
              insertEditedNode(editedChild, editedNodes[i - 1]);
          }
          else if (editedChild.type === "opaque") {
              insertEditedNode(editedChild, editedNodes[i - 1]);
          }
          // We intentially don't preserve PlasmicTagOrComponent nodes that are moved
          // from other places here (note that if the nodes were here before, then
          // merged should already contain that node). The reason is that it is easy
          // to do so in Plasmic, and that it enforces the consistency between Plasmic
          // studio and code.
      });
      return common_1.withoutNils(merged.map((c) => {
          if (c.type === "opaque") {
              return c.rawNode;
          }
          // Note that, if c is PlasmicTagOrComponent, it must come from newNodes.
          const n = serializeNonOpaquePlasmicASTNode(c, codeVersions);
          if (!n) {
              return undefined;
          }
          if (babel.types.isExpression(n)) {
              // need to wrap in expression container
              return n.type !== "JSXElement" && n.type !== "JSXFragment"
                  ? babel.types.jsxExpressionContainer(n)
                  : n;
          }
          return n;
      }));
  };
  const mergedChildren = (newNode, editedNode, baseNode, codeVersions) => {
      return mergeNodes(newNode.jsxElement.children, editedNode.jsxElement.children, baseNode.jsxElement.children, codeVersions);
  };
  const makeJsxElementWithShowCall = (jsxElement, nodeId) => babel.types.logicalExpression("&&", plasmic_ast_1.makeCallExpression(plasmic_parser_1.helperObject, `show${nodeId}`), jsxElement);
  const serializeTagOrComponent = (newNode, codeVersions) => {
      const { newVersion, editedVersion, baseVersion } = codeVersions;
      // find node with same id in edited version.
      const editedNode = editedVersion.findTagOrComponent(newNode.jsxElement.nameInId);
      const baseNode = baseVersion.findTagOrComponent(newNode.jsxElement.nameInId);
      if (editedNode) {
          // the node must exist in base version.
          common_1.assert(!!baseNode);
          const editedNodeJsxElementClone = babel.types.cloneDeep(editedNode.jsxElement.rawNode);
          editedNodeJsxElementClone.openingElement.name = mergedTag(newNode, editedNode, baseNode);
          if (editedNodeJsxElementClone.closingElement) {
              editedNodeJsxElementClone.closingElement.name =
                  editedNodeJsxElementClone.openingElement.name;
          }
          editedNodeJsxElementClone.openingElement.attributes = mergeAttributes(newNode, editedNode, baseNode, codeVersions);
          editedNodeJsxElementClone.children = mergedChildren(newNode, editedNode, baseNode, codeVersions);
          if (!editedNodeJsxElementClone.closingElement &&
              editedNodeJsxElementClone.children.length > 0) {
              editedNodeJsxElementClone.closingElement = babel.types.jsxClosingElement(editedNodeJsxElementClone.openingElement.name);
              editedNodeJsxElementClone.openingElement.selfClosing = false;
          }
          const secondaryNodes = new Map(editedNode.secondaryNodes.map((n) => {
              const newSecondaryNode = newVersion.findTagOrComponent(n.jsxElement.nameInId);
              if (!newSecondaryNode) {
                  return [n.rawNode, undefined];
              }
              const rawReplacement = serializePlasmicASTNode(newSecondaryNode, codeVersions);
              return [n.rawNode, rawReplacement];
          }));
          const newNodeCallShowFunc = newVersion.hasShowFuncCall(newNode);
          const editedNodeCallShowFunc = editedVersion.hasShowFuncCall(editedNode);
          const editedNodeClone = cloneDeepWithHook_1.cloneDeepWithHook(editedNode.rawNode, (n) => {
              if (n === editedNode.jsxElement.rawNode) {
                  if (newNodeCallShowFunc && !editedNodeCallShowFunc) {
                      // add the show call
                      const expr = makeJsxElementWithShowCall(editedNodeJsxElementClone, editedNode.jsxElement.nameInId);
                      // add an expression container if the parent is JSXElement or Fragment
                      return editedNode.jsxElement.rawParent &&
                          plasmic_parser_1.isJsxElementOrFragment(editedNode.jsxElement.rawParent)
                          ? plasmic_ast_1.wrapInJsxExprContainer(expr)
                          : expr;
                  }
                  return editedNodeJsxElementClone;
              }
              if (secondaryNodes.has(n)) {
                  const replacement = secondaryNodes.get(n);
                  // If deleted, an empty fragment instead
                  return replacement || mkJsxFragment([]);
              }
              return undefined;
          });
          if (editedNodeCallShowFunc && !newNodeCallShowFunc) {
              traverse_1.default(editedNodeClone, {
                  noScope: true,
                  CallExpression: function (path) {
                      if (plasmic_parser_1.isCallIgnoreArguments(path.node, plasmic_parser_1.helperObject, `show${editedNode.jsxElement.nameInId}`)) {
                          path.replaceWithSourceString("true");
                      }
                  },
              });
          }
          return editedNodeClone;
      }
      // check if the node has been deleted.
      if (baseNode) {
          // If so, don't output anything
          return undefined;
      }
      // This is new node. Just output self.
      const childrenReplacement = new Map();
      newNode.jsxElement.children.forEach((child) => {
          // Plasmic never emit opaque node.
          common_1.assert(child.type !== "opaque");
          const childReplacement = serializeNonOpaquePlasmicASTNode(child, codeVersions);
          if (childReplacement) {
              if (babel.types.isExpression(childReplacement)) {
                  // need to wrap in expression container
                  const maybeWrapped = childReplacement.type !== "JSXElement" &&
                      childReplacement.type !== "JSXFragment"
                      ? babel.types.jsxExpressionContainer(childReplacement)
                      : childReplacement;
                  childrenReplacement.set(child.rawNode, maybeWrapped);
              }
              else {
                  childrenReplacement.set(child.rawNode, childReplacement);
              }
          }
      });
      // Attribute replacement
      const attrsReplacement = new Map();
      newNode.jsxElement.attrs.forEach((attr) => {
          if (L.isArray(attr)) {
              const [key, value] = attr;
              // className is an opaque attribute!
              if (value && value.type !== "opaque") {
                  const attrReplacement = serializeNonOpaquePlasmicASTNode(value, codeVersions);
                  if (attrReplacement) {
                      if (attrReplacement.type !== "JSXExpressionContainer") {
                          common_1.assert(attrReplacement.type !== "JSXText");
                          attrsReplacement.set(value.rawNode, babel.types.jsxExpressionContainer(attrReplacement));
                      }
                      else {
                          attrsReplacement.set(value.rawNode, attrReplacement);
                      }
                  }
              }
          }
      });
      return cloneDeepWithHook_1.cloneDeepWithHook(newNode.rawNode, (n) => childrenReplacement.get(n) || attrsReplacement.get(n));
  };
  const serializeNonOpaquePlasmicASTNode = (newNode, codeVersions) => {
      common_1.assert(newNode.type !== "opaque");
      if (newNode.type === "child-str-call") {
          // Just output the new version
          return newNode.rawNode;
      }
      else if (newNode.type === "string-lit") {
          // Just output the new version
          return newNode.rawNode;
      }
      else if (newNode.type === "text") {
          // Just output the new version
          return newNode.rawNode;
      }
      else if (newNode.type === "jsx-fragment") {
          return babel.types.jsxFragment(babel.types.jsxOpeningFragment(), babel.types.jsxClosingFragment(), common_1.withoutNils(newNode.children.map((child) => {
              const newRawNode = serializePlasmicASTNode(child, codeVersions);
              if (!newRawNode) {
                  return undefined;
              }
              if (babel.types.isExpression(newRawNode) ||
                  babel.types.isJSXEmptyExpression(newRawNode)) {
                  return babel.types.jsxExpressionContainer(newRawNode);
              }
              return newRawNode;
          })));
      }
      else {
          common_1.assert(newNode.type === "tag-or-component");
          return serializeTagOrComponent(newNode, codeVersions);
      }
  };
  const serializePlasmicASTNode = (newNode, codeVersions) => {
      if (newNode.type === "opaque") {
          return newNode.rawNode;
      }
      return serializeNonOpaquePlasmicASTNode(newNode, codeVersions);
  };
  exports.renameAndSerializePlasmicASTNode = (newNode, codeVersions) => {
      if (newNode.type === "opaque") {
          return newNode.rawNode;
      }
      return serializeNonOpaquePlasmicASTNode(newNode, {
          baseVersion: codeVersions.baseVersion.renameJsxTree(codeVersions.newVersion),
          editedVersion: codeVersions.editedVersion.renameJsxTree(codeVersions.newVersion),
          newVersion: codeVersions.newVersion,
      });
  };
  class ComponentSkeletonModel {
      constructor(uuid, nameInIdToUuid, fileContent) {
          this.uuid = uuid;
          this.nameInIdToUuid = nameInIdToUuid;
          this.fileContent = fileContent;
      }
      toJSON() {
          return {
              uuid: this.uuid,
              nameInIdToUuid: [...this.nameInIdToUuid.entries()],
              fileContent: this.fileContent,
          };
      }
      static fromJsObject(jsObj) {
          return new ComponentSkeletonModel(jsObj.uuid, new Map(jsObj.nameInIdToUuid), jsObj.fileContent);
      }
  }
  exports.ComponentSkeletonModel = ComponentSkeletonModel;
  class ProjectSyncMetadataModel {
      constructor(components) {
          this.components = components;
      }
      toJSON() {
          return this.components;
      }
      static fromJson(json) {
          const j = JSON.parse(json);
          common_1.assert(L.isArray(j));
          return new ProjectSyncMetadataModel(j.map((item) => ComponentSkeletonModel.fromJsObject(item)));
      }
  }
  exports.ProjectSyncMetadataModel = ProjectSyncMetadataModel;
  exports.makeCachedProjectSyncDataProvider = (rawProvider) => {
      const cache = [];
      return async (projectId, revision) => {
          const cached = cache.find((ent) => ent.projectId === projectId && ent.revision === revision);
          if (cached) {
              return cached.model;
          }
          const model = await rawProvider(projectId, revision);
          cache.push({ projectId, revision, model });
          return model;
      };
  };
  const tryExtractPlasmicJsxExpression = (node) => {
      for (const c of node.leadingComments || []) {
          const m = c.value.match(/^\s*plasmic-managed-jsx\/(\d+)\s*$/);
          if (m) {
              if (node.type === "AssignmentExpression") {
                  return { jsx: node.right, identifyingComment: c, revision: +m[1] };
              }
              else {
                  return {
                      jsx: common_1.ensure(node.argument),
                      identifyingComment: c,
                      revision: +m[1],
                  };
              }
          }
      }
      return undefined;
  };
  const tryParseComponentSkeletonFile = (fileContent) => {
      const file = parser.parse(fileContent, {
          strictMode: true,
          sourceType: "module",
          plugins: ["jsx", "typescript"],
      });
      let jsx = undefined;
      traverse_1.default(file, {
          noScope: true,
          AssignmentExpression: function (path) {
              jsx = tryExtractPlasmicJsxExpression(path.node);
              if (jsx) {
                  path.stop();
              }
          },
          ReturnStatement: function (path) {
              jsx = tryExtractPlasmicJsxExpression(path.node);
              if (jsx) {
                  path.stop();
              }
          },
      });
      // typescript treat jsx as never type... why?
      jsx = jsx;
      return jsx ? { ...jsx, file } : undefined;
  };
  // merge slave into master
  const mergeImports = (editedImport, newImport) => {
      if (editedImport.specifiers.find((s) => s.type === "ImportNamespaceSpecifier")) {
          return lodash_1.cloneDeep(editedImport);
      }
      if (newImport.specifiers.find((s) => s.type === "ImportNamespaceSpecifier")) {
          return lodash_1.cloneDeep(newImport);
      }
      const cloned = lodash_1.cloneDeep(editedImport);
      for (const s2 of newImport.specifiers) {
          if (s2.type === "ImportDefaultSpecifier") {
              if (editedImport.specifiers.find((s1) => s1.type === "ImportDefaultSpecifier" &&
                  s1.local.name === s2.local.name)) {
                  continue;
              }
              cloned.specifiers.push(s2);
          }
          else if (s2.type === "ImportSpecifier") {
              if (editedImport.specifiers.find((s1) => s1.type === "ImportSpecifier" &&
                  s1.local.name === s2.local.name &&
                  importedName(s1) === importedName(s2))) {
                  continue;
              }
              cloned.specifiers.push(s2);
          }
          else {
              common_1.assert(s2.type === "ImportNamespaceSpecifier");
              // Plasmic doesn't generate namespace import statement.
              cloned.specifiers.push(s2);
          }
      }
      return cloned;
  };
  function importedName(stmt) {
      if (stmt.imported.type === "Identifier") {
          return stmt.imported.name;
      }
      else {
          return stmt.imported.value;
      }
  }
  const mergePlasmicImports = (mergedFile, parsedNew, parsedEdited) => {
      const newImports = parsedNew.file.program.body.filter((stmt) => stmt.type === "ImportDeclaration");
      const editedImports = parsedEdited.file.program.body.filter((stmt) => stmt.type === "ImportDeclaration");
      const firstImport = mergedFile.program.body.findIndex((stmt) => stmt.type === "ImportDeclaration");
      mergedFile.program.body = mergedFile.program.body.filter((stmt) => stmt.type !== "ImportDeclaration");
      const mergedImports = [];
      for (const editedImport of editedImports) {
          const newImportAt = newImports.findIndex((newImport) => editedImport.source.value === newImport.source.value);
          if (newImportAt !== -1) {
              const newImport = newImports[newImportAt];
              newImports.splice(newImportAt, 1);
              mergedImports.push(mergeImports(editedImport, newImport));
          }
          else {
              mergedImports.push(editedImport);
          }
      }
      mergedImports.push(...newImports);
      const insertMergedImportsAt = firstImport > -1 ? firstImport : 0;
      mergedFile.program.body.splice(insertMergedImportsAt, 0, ...mergedImports);
  };
  class WarningInfo {
      constructor() {
          this._rawWarnings = [];
          this._secondaryNodes = [];
      }
      addRawWarn(msg) {
          this._rawWarnings.push(msg);
      }
      setSecondaryNodes(nodes) {
          this._secondaryNodes.push(...nodes);
      }
      rawWarnings() {
          return this._rawWarnings;
      }
      secondaryNodes() {
          return this._secondaryNodes;
      }
      maybeWarn() {
          this._rawWarnings.forEach((m) => console.warn(m));
          if (this._secondaryNodes.length > 0) {
              const nodes = this._secondaryNodes
                  .map((n) => n.jsxElement.nameInId)
                  .join("\n\t");
              console.warn(`Plasmic perform limited merge to the following nodes since they are secondary nodes.\n${nodes}`);
          }
      }
  }
  exports.WarningInfo = WarningInfo;
  exports.mergeFiles = async (componentByUuid, projectId, projectSyncDataProvider, preMergeFile, appendJsxTreeOnMissingBase, 
  // Output parameter, which is used to collect warning information
  warningInfos) => {
      const updateableByComponentUuid = new Map();
      componentByUuid.forEach((codeVersions, uuid) => {
          const parsedEdited = tryParseComponentSkeletonFile(codeVersions.editedFile);
          if (parsedEdited) {
              updateableByComponentUuid.set(uuid, parsedEdited);
          }
      });
      if (updateableByComponentUuid.size === 0) {
          // Nothing to update
          return undefined;
      }
      const mergedFiles = new Map();
      for (const [componentUuid, parsedEdited] of updateableByComponentUuid) {
          const warnInfo = new WarningInfo();
          warningInfos === null || warningInfos === void 0 ? void 0 : warningInfos.set(componentUuid, warnInfo);
          let baseMetadata = undefined;
          try {
              const projectSyncData = await projectSyncDataProvider(projectId, parsedEdited.revision);
              baseMetadata = projectSyncData.components.find((c) => c.uuid === componentUuid);
          }
          catch {
              warnInfo.addRawWarn(`missing merging base for ${projectId} at revision ${parsedEdited.revision}`);
          }
          const component = common_1.ensure(componentByUuid.get(componentUuid));
          const parsedNew = common_1.ensure(tryParseComponentSkeletonFile(component.newFile));
          if (!baseMetadata) {
              if (appendJsxTreeOnMissingBase) {
                  mergedFiles.set(componentUuid, utils_1.formatted(`${component.editedFile}
  
            // Please perform merge with the following JSX manually.
             \`// plasmic-managed-jsx/${parsedNew.revision}
    return (${utils_1.code(parsedNew.jsx).trimEnd()});\``));
                  continue;
              }
              else {
                  throw new Error(`Cannot perform three way merge due to missing base version. For Plasmic CLI users, please add '--append-jsx-on-missing-base' so that you can perform merging by yourselves.`);
              }
          }
          if (preMergeFile) {
              preMergeFile(componentUuid, baseMetadata.fileContent, baseMetadata.nameInIdToUuid, component.newFile, component.newNameInIdToUuid);
          }
          const parsedBase = common_1.ensure(tryParseComponentSkeletonFile(baseMetadata.fileContent));
          const newCodeVersion = new plasmic_parser_1.CodeVersion(parsedNew.jsx, component.newNameInIdToUuid);
          const baseCodeVersion = new plasmic_parser_1.CodeVersion(parsedBase.jsx, baseMetadata.nameInIdToUuid);
          // All other metadata
          const editedCodeVersion = new plasmic_parser_1.CodeVersion(parsedEdited.jsx, 
          // edited version share the same nameInIdtoUuid mapping
          baseMetadata.nameInIdToUuid);
          warnInfo.setSecondaryNodes([
              ...editedCodeVersion.secondaryTagsOrComponents.values(),
          ]);
          const newJsx = exports.renameAndSerializePlasmicASTNode(newCodeVersion.root, {
              newVersion: newCodeVersion,
              editedVersion: editedCodeVersion,
              baseVersion: baseCodeVersion,
          });
          // Ideally, we should keep parsedEdited read-only, but, it is not a big deal
          // to modify the comment.
          parsedEdited.identifyingComment.value = ` plasmic-managed-jsx/${parsedNew.revision}`;
          const mergedFile = cloneDeepWithHook_1.cloneDeepWithHook(parsedEdited.file, (n) => {
              if (n === parsedEdited.jsx) {
                  return newJsx;
              }
              return undefined;
          });
          mergePlasmicImports(mergedFile, parsedNew, parsedEdited);
          const mergedCode = utils_1.code(mergedFile, { retainLines: true });
          mergedFiles.set(componentUuid, utils_1.formatted(mergedCode));
      }
      return mergedFiles;
  };
  